loading
Generated 2026-05-15T13:10:45+00:00

All Files ( 100.0% covered at 7.95 hits/line )

373 files in total.
5741 relevant lines, 5741 lines covered and 0 lines missed. ( 100.0% )
680 total branches, 680 branches covered and 0 branches missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
app/action.rb 100.00 % 44 16 16 0 164.69 100.00 % 2 2 0
app/actions/api/base.rb 100.00 % 52 27 27 0 11.11 100.00 % 0 0 0
app/actions/api/devices/create.rb 100.00 % 63 25 25 0 1.44 100.00 % 4 4 0
app/actions/api/devices/delete.rb 100.00 % 20 10 10 0 1.20 100.00 % 0 0 0
app/actions/api/devices/index.rb 100.00 % 20 10 10 0 1.30 100.00 % 0 0 0
app/actions/api/devices/patch.rb 100.00 % 44 19 19 0 1.37 100.00 % 2 2 0
app/actions/api/devices/show.rb 100.00 % 25 12 12 0 1.17 100.00 % 2 2 0
app/actions/api/display/show.rb 100.00 % 100 39 39 0 2.46 100.00 % 6 6 0
app/actions/api/firmware/create.rb 100.00 % 84 38 38 0 1.24 100.00 % 2 2 0
app/actions/api/firmware/delete.rb 100.00 % 35 18 18 0 1.11 100.00 % 2 2 0
app/actions/api/firmware/index.rb 100.00 % 20 10 10 0 1.30 100.00 % 0 0 0
app/actions/api/firmware/patch.rb 100.00 % 91 42 42 0 1.40 100.00 % 4 4 0
app/actions/api/firmware/show.rb 100.00 % 25 12 12 0 1.17 100.00 % 2 2 0
app/actions/api/log/create.rb 100.00 % 93 47 47 0 1.96 100.00 % 4 4 0
app/actions/api/models/create.rb 100.00 % 61 34 34 0 1.06 100.00 % 2 2 0
app/actions/api/models/delete.rb 100.00 % 20 10 10 0 1.20 100.00 % 0 0 0
app/actions/api/models/index.rb 100.00 % 20 10 10 0 1.30 100.00 % 0 0 0
app/actions/api/models/patch.rb 100.00 % 63 35 35 0 1.06 100.00 % 2 2 0
app/actions/api/models/show.rb 100.00 % 25 12 12 0 1.17 100.00 % 2 2 0
app/actions/api/playlists/create.rb 100.00 % 63 29 29 0 1.34 100.00 % 2 2 0
app/actions/api/playlists/delete.rb 100.00 % 26 13 13 0 1.15 100.00 % 2 2 0
app/actions/api/playlists/index.rb 100.00 % 20 10 10 0 1.30 100.00 % 0 0 0
app/actions/api/playlists/patch.rb 100.00 % 60 30 30 0 1.33 100.00 % 2 2 0
app/actions/api/playlists/show.rb 100.00 % 25 12 12 0 1.17 100.00 % 2 2 0
app/actions/api/screens/create.rb 100.00 % 95 43 43 0 2.63 100.00 % 8 8 0
app/actions/api/screens/delete.rb 100.00 % 35 18 18 0 1.11 100.00 % 2 2 0
app/actions/api/screens/index.rb 100.00 % 21 10 10 0 1.10 100.00 % 0 0 0
app/actions/api/screens/patch.rb 100.00 % 88 41 41 0 1.85 100.00 % 6 6 0
app/actions/api/setup/show.rb 100.00 % 75 29 29 0 1.76 100.00 % 4 4 0
app/actions/bulk/devices/logs/delete.rb 100.00 % 27 13 13 0 1.23 100.00 % 2 2 0
app/actions/bulk/firmware/delete.rb 100.00 % 19 9 9 0 1.22 100.00 % 0 0 0
app/actions/dashboard/show.rb 100.00 % 20 8 8 0 7.75 100.00 % 0 0 0
app/actions/designer/create.rb 100.00 % 57 26 26 0 2.38 100.00 % 6 6 0
app/actions/designer/show.rb 100.00 % 12 5 5 0 1.00 100.00 % 0 0 0
app/actions/devices/create.rb 100.00 % 47 16 16 0 1.69 100.00 % 4 4 0
app/actions/devices/delete.rb 100.00 % 23 11 11 0 1.64 100.00 % 2 2 0
app/actions/devices/edit.rb 100.00 % 38 13 13 0 1.85 100.00 % 2 2 0
app/actions/devices/index.rb 100.00 % 36 18 18 0 3.44 100.00 % 6 6 0
app/actions/devices/logs/delete.rb 100.00 % 28 14 14 0 1.14 100.00 % 2 2 0
app/actions/devices/logs/index.rb 100.00 % 65 30 30 0 2.40 100.00 % 8 8 0
app/actions/devices/logs/show.rb 100.00 % 31 14 14 0 1.00 100.00 % 0 0 0
app/actions/devices/new.rb 100.00 % 25 7 7 0 1.29 100.00 % 0 0 0
app/actions/devices/show.rb 100.00 % 24 10 10 0 1.90 100.00 % 2 2 0
app/actions/devices/update.rb 100.00 % 51 20 20 0 1.35 100.00 % 4 4 0
app/actions/extensions/build/create.rb 100.00 % 31 16 16 0 1.69 100.00 % 0 0 0
app/actions/extensions/clone/create.rb 100.00 % 55 25 25 0 1.36 100.00 % 4 4 0
app/actions/extensions/clone/new.rb 100.00 % 23 11 11 0 1.09 100.00 % 0 0 0
app/actions/extensions/create.rb 100.00 % 55 25 25 0 1.80 100.00 % 2 2 0
app/actions/extensions/delete.rb 100.00 % 22 11 11 0 1.64 100.00 % 2 2 0
app/actions/extensions/edit.rb 100.00 % 22 10 10 0 2.50 100.00 % 2 2 0
app/actions/extensions/exchanges/create.rb 100.00 % 54 22 22 0 1.36 100.00 % 2 2 0
app/actions/extensions/exchanges/delete.rb 100.00 % 30 15 15 0 1.47 100.00 % 2 2 0
app/actions/extensions/exchanges/edit.rb 100.00 % 41 16 16 0 1.63 100.00 % 2 2 0
app/actions/extensions/exchanges/index.rb 100.00 % 29 12 12 0 2.00 100.00 % 0 0 0
app/actions/extensions/exchanges/new.rb 100.00 % 34 14 14 0 1.79 100.00 % 2 2 0
app/actions/extensions/exchanges/update.rb 100.00 % 56 24 24 0 1.17 100.00 % 2 2 0
app/actions/extensions/export/show.rb 100.00 % 38 18 18 0 1.67 100.00 % 4 4 0
app/actions/extensions/gallery/index.rb 100.00 % 55 30 30 0 2.93 100.00 % 6 6 0
app/actions/extensions/index.rb 100.00 % 34 17 17 0 3.94 100.00 % 6 6 0
app/actions/extensions/new.rb 100.00 % 25 12 12 0 1.50 100.00 % 0 0 0
app/actions/extensions/preview/show.rb 100.00 % 45 21 21 0 2.76 100.00 % 5 5 0
app/actions/extensions/sensors/index.rb 100.00 % 40 19 19 0 4.11 100.00 % 2 2 0
app/actions/extensions/sources/index.rb 100.00 % 37 17 17 0 4.24 100.00 % 0 0 0
app/actions/extensions/update.rb 100.00 % 59 30 30 0 1.23 100.00 % 4 4 0
app/actions/firmware/create.rb 100.00 % 53 24 24 0 1.25 100.00 % 2 2 0
app/actions/firmware/delete.rb 100.00 % 23 11 11 0 2.36 100.00 % 2 2 0
app/actions/firmware/edit.rb 100.00 % 24 10 10 0 2.20 100.00 % 2 2 0
app/actions/firmware/index.rb 100.00 % 32 16 16 0 4.38 100.00 % 4 4 0
app/actions/firmware/new.rb 100.00 % 16 7 7 0 1.29 100.00 % 0 0 0
app/actions/firmware/show.rb 100.00 % 24 10 10 0 2.20 100.00 % 2 2 0
app/actions/firmware/update.rb 100.00 % 63 32 32 0 1.59 100.00 % 6 6 0
app/actions/models/clone/create.rb 100.00 % 42 20 20 0 1.40 100.00 % 4 4 0
app/actions/models/clone/new.rb 100.00 % 21 10 10 0 1.00 100.00 % 0 0 0
app/actions/models/create.rb 100.00 % 41 16 16 0 1.63 100.00 % 2 2 0
app/actions/models/delete.rb 100.00 % 23 11 11 0 1.64 100.00 % 2 2 0
app/actions/models/edit.rb 100.00 % 24 10 10 0 1.90 100.00 % 2 2 0
app/actions/models/index.rb 100.00 % 34 17 17 0 3.94 100.00 % 6 6 0
app/actions/models/new.rb 100.00 % 25 12 12 0 1.50 100.00 % 0 0 0
app/actions/models/show.rb 100.00 % 24 10 10 0 1.60 100.00 % 2 2 0
app/actions/models/update.rb 100.00 % 44 20 20 0 1.35 100.00 % 4 4 0
app/actions/playlists/clone/create.rb 100.00 % 50 25 25 0 1.32 100.00 % 4 4 0
app/actions/playlists/clone/new.rb 100.00 % 21 10 10 0 1.00 100.00 % 0 0 0
app/actions/playlists/create.rb 100.00 % 45 19 19 0 1.53 100.00 % 2 2 0
app/actions/playlists/delete.rb 100.00 % 23 11 11 0 1.64 100.00 % 2 2 0
app/actions/playlists/edit.rb 100.00 % 38 14 14 0 1.93 100.00 % 2 2 0
app/actions/playlists/index.rb 100.00 % 34 17 17 0 4.53 100.00 % 6 6 0
app/actions/playlists/items/create.rb 100.00 % 42 19 19 0 1.37 100.00 % 2 2 0
app/actions/playlists/items/index.rb 100.00 % 21 10 10 0 1.30 100.00 % 0 0 0
app/actions/playlists/mirror/edit.rb 100.00 % 31 11 11 0 2.09 100.00 % 2 2 0
app/actions/playlists/mirror/update.rb 100.00 % 52 22 22 0 2.41 100.00 % 2 2 0
app/actions/playlists/new.rb 100.00 % 16 7 7 0 1.29 100.00 % 0 0 0
app/actions/playlists/screens/index.rb 100.00 % 34 17 17 0 1.53 100.00 % 2 2 0
app/actions/playlists/screens/show.rb 100.00 % 58 24 24 0 3.92 100.00 % 4 4 0
app/actions/playlists/show.rb 100.00 % 38 14 14 0 1.57 100.00 % 2 2 0
app/actions/playlists/update.rb 100.00 % 59 25 25 0 1.28 100.00 % 4 4 0
app/actions/problem_details/index.rb 100.00 % 12 5 5 0 1.00 100.00 % 0 0 0
app/actions/screens/create.rb 100.00 % 59 27 27 0 1.07 100.00 % 2 2 0
app/actions/screens/delete.rb 100.00 % 23 11 11 0 1.64 100.00 % 2 2 0
app/actions/screens/edit.rb 100.00 % 29 10 10 0 2.20 100.00 % 2 2 0
app/actions/screens/index.rb 100.00 % 34 17 17 0 3.35 100.00 % 6 6 0
app/actions/screens/new.rb 100.00 % 16 7 7 0 1.29 100.00 % 0 0 0
app/actions/screens/show.rb 100.00 % 24 10 10 0 2.20 100.00 % 2 2 0
app/actions/screens/update.rb 100.00 % 68 35 35 0 1.54 100.00 % 6 6 0
app/actions/users/create.rb 100.00 % 39 12 12 0 1.42 100.00 % 2 2 0
app/actions/users/edit.rb 100.00 % 29 10 10 0 1.90 100.00 % 2 2 0
app/actions/users/index.rb 100.00 % 34 17 17 0 3.06 100.00 % 6 6 0
app/actions/users/new.rb 100.00 % 16 7 7 0 1.29 100.00 % 0 0 0
app/actions/users/show.rb 100.00 % 24 10 10 0 1.60 100.00 % 2 2 0
app/actions/users/update.rb 100.00 % 39 13 13 0 1.08 100.00 % 2 2 0
app/aspects/croner.rb 100.00 % 74 43 43 0 3.79 100.00 % 20 20 0
app/aspects/devices/defaulter.rb 100.00 % 33 12 12 0 10.17 100.00 % 0 0 0
app/aspects/devices/mac_address_builder.rb 100.00 % 20 10 10 0 52.20 100.00 % 0 0 0
app/aspects/devices/provisioner.rb 100.00 % 68 33 33 0 13.06 100.00 % 2 2 0
app/aspects/devices/sensors/synchronizer.rb 100.00 % 91 44 44 0 3.30 100.00 % 6 6 0
app/aspects/devices/synchronizer.rb 100.00 % 34 15 15 0 2.87 100.00 % 2 2 0
app/aspects/downloader.rb 100.00 % 38 20 20 0 5.75 100.00 % 6 6 0
app/aspects/extensions/cloner.rb 100.00 % 77 32 32 0 3.91 100.00 % 0 0 0
app/aspects/extensions/contextualizer.rb 100.00 % 34 14 14 0 9.36 100.00 % 2 2 0
app/aspects/extensions/curler.rb 100.00 % 54 24 24 0 4.58 100.00 % 8 8 0
app/aspects/extensions/defaults.rb 100.00 % 25 4 4 0 1.00 100.00 % 0 0 0
app/aspects/extensions/exchanges/coalescer.rb 100.00 % 19 9 9 0 7.33 100.00 % 0 0 0
app/aspects/extensions/exchanges/refresher.rb 100.00 % 66 33 33 0 3.42 100.00 % 5 5 0
app/aspects/extensions/exporter.rb 100.00 % 27 10 10 0 1.40 100.00 % 0 0 0
app/aspects/extensions/fetchers/input.rb 100.00 % 18 8 8 0 5.75 100.00 % 0 0 0
app/aspects/extensions/fetchers/sole.rb 100.00 % 80 42 42 0 4.79 100.00 % 10 10 0
app/aspects/extensions/importers/remote/creator.rb 100.00 % 83 39 39 0 3.28 100.00 % 5 5 0
app/aspects/extensions/importers/remote/extractor.rb 100.00 % 51 26 26 0 1.62 100.00 % 0 0 0
app/aspects/extensions/importers/remote/schema.rb 100.00 % 30 19 19 0 1.00 100.00 % 0 0 0
app/aspects/extensions/importers/remote/transformer.rb 100.00 % 59 27 27 0 2.59 100.00 % 2 2 0
app/aspects/extensions/importers/remote/transformers/data.rb 100.00 % 37 19 19 0 2.05 100.00 % 2 2 0
app/aspects/extensions/importers/remote/transformers/default.rb 100.00 % 34 15 15 0 1.27 100.00 % 0 0 0
app/aspects/extensions/importers/remote/transformers/keys.rb 100.00 % 43 17 17 0 2.00 100.00 % 0 0 0
app/aspects/extensions/importers/remote/transformers/kind.rb 100.00 % 55 28 28 0 2.82 100.00 % 4 4 0
app/aspects/extensions/importers/remote/transformers/poll.rb 100.00 % 41 19 19 0 2.63 100.00 % 2 2 0
app/aspects/extensions/importers/remote/transformers/template.rb 100.00 % 46 20 20 0 2.55 100.00 % 0 0 0
app/aspects/extensions/importers/remote/transformers/template_keys.rb 100.00 % 58 23 23 0 6.70 100.00 % 0 0 0
app/aspects/extensions/parser.rb 100.00 % 49 27 27 0 2.56 100.00 % 4 4 0
app/aspects/extensions/renderer.rb 100.00 % 40 19 19 0 4.00 100.00 % 4 4 0
app/aspects/extensions/renderers/image.rb 100.00 % 46 20 20 0 1.15 100.00 % 2 2 0
app/aspects/extensions/renderers/poll.rb 100.00 % 42 21 21 0 4.86 100.00 % 0 0 0
app/aspects/extensions/renderers/static.rb 100.00 % 25 11 11 0 1.18 100.00 % 0 0 0
app/aspects/extensions/screen_upserter.rb 100.00 % 27 9 9 0 3.33 100.00 % 0 0 0
app/aspects/extensions/uri_builder.rb 100.00 % 17 8 8 0 6.25 100.00 % 0 0 0
app/aspects/firmware/headers/model.rb 100.00 % 54 15 15 0 6.47 100.00 % 0 0 0
app/aspects/firmware/headers/parser.rb 100.00 % 46 20 20 0 6.75 100.00 % 0 0 0
app/aspects/firmware/headers/transformers/model_name.rb 100.00 % 56 20 20 0 7.60 100.00 % 2 2 0
app/aspects/firmware/headers/transformers/sensors.rb 100.00 % 59 29 29 0 7.69 100.00 % 2 2 0
app/aspects/firmware/log_transformer.rb 100.00 % 24 12 12 0 1.75 100.00 % 0 0 0
app/aspects/firmware/models/setup.rb 100.00 % 27 12 12 0 2.67 100.00 % 0 0 0
app/aspects/firmware/synchronizer.rb 100.00 % 48 24 24 0 2.17 100.00 % 6 6 0
app/aspects/fonts/synchronizer.rb 100.00 % 54 28 28 0 18.46 100.00 % 2 2 0
app/aspects/jobs/schedule.rb 100.00 % 36 18 18 0 5.56 100.00 % 6 6 0
app/aspects/json_formatter.rb 100.00 % 23 12 12 0 15.67 100.00 % 3 3 0
app/aspects/logging/rack_adapter.rb 100.00 % 23 11 11 0 2.00 100.00 % 0 0 0
app/aspects/models/cloner.rb 100.00 % 39 18 18 0 2.44 100.00 % 0 0 0
app/aspects/models/defaults.rb 100.00 % 20 4 4 0 1.00 100.00 % 0 0 0
app/aspects/models/finder.rb 100.00 % 37 16 16 0 23.81 100.00 % 6 6 0
app/aspects/models/palette_optioner.rb 100.00 % 36 15 15 0 5.00 100.00 % 4 4 0
app/aspects/models/synchronizer.rb 100.00 % 85 42 42 0 7.48 100.00 % 8 8 0
app/aspects/palettes/synchronizer.rb 100.00 % 57 31 31 0 2.35 100.00 % 4 4 0
app/aspects/password_encryptor.rb 100.00 % 22 9 9 0 28.11 100.00 % 2 2 0
app/aspects/playlists/cloner.rb 100.00 % 62 30 30 0 3.60 100.00 % 4 4 0
app/aspects/playlists/screen_optioner.rb 100.00 % 18 8 8 0 1.38 100.00 % 0 0 0
app/aspects/playlists/slide_window.rb 100.00 % 36 17 17 0 6.00 100.00 % 4 4 0
app/aspects/problem_detail.rb 100.00 % 60 20 20 0 1.45 100.00 % 0 0 0
app/aspects/sanitizer.rb 100.00 % 37 18 18 0 50.89 100.00 % 0 0 0
app/aspects/screens/converter.rb 100.00 % 14 6 6 0 11.33 100.00 % 2 2 0
app/aspects/screens/converters/color.rb 100.00 % 55 28 28 0 4.21 100.00 % 4 4 0
app/aspects/screens/converters/monochrome.rb 100.00 % 88 53 53 0 25.00 100.00 % 10 10 0
app/aspects/screens/designer/event_stream.rb 100.00 % 57 27 27 0 2.48 100.00 % 2 2 0
app/aspects/screens/designer/middleware.rb 100.00 % 39 14 14 0 88.07 100.00 % 2 2 0
app/aspects/screens/fetcher.rb 100.00 % 47 21 21 0 7.29 100.00 % 6 6 0
app/aspects/screens/find_or_creator.rb 100.00 % 38 18 18 0 9.28 100.00 % 2 2 0
app/aspects/screens/gaffer.rb 100.00 % 18 7 7 0 1.43 100.00 % 0 0 0
app/aspects/screens/mold.rb 100.00 % 50 15 15 0 6.27 100.00 % 2 2 0
app/aspects/screens/mold_builder.rb 100.00 % 51 27 27 0 27.85 100.00 % 2 2 0
app/aspects/screens/placeholder.rb 100.00 % 37 17 17 0 70.18 100.00 % 0 0 0
app/aspects/screens/rotator.rb 100.00 % 52 24 24 0 4.92 100.00 % 8 8 0
app/aspects/screens/shoter.rb 100.00 % 102 55 55 0 23.76 100.00 % 2 2 0
app/aspects/screens/sleeper.rb 100.00 % 18 7 7 0 1.43 100.00 % 0 0 0
app/aspects/screens/temp_pather.rb 100.00 % 35 19 19 0 24.26 100.00 % 2 2 0
app/aspects/screens/upserter.rb 100.00 % 38 17 17 0 6.00 100.00 % 4 4 0
app/aspects/screens/upserters/html.rb 100.00 % 25 13 13 0 4.38 100.00 % 0 0 0
app/aspects/screens/upserters/preprocessed.rb 100.00 % 35 18 18 0 6.78 100.00 % 0 0 0
app/aspects/screens/upserters/unprocessed.rb 100.00 % 47 22 22 0 5.77 100.00 % 0 0 0
app/aspects/screens/welcomer.rb 100.00 % 18 7 7 0 4.57 100.00 % 0 0 0
app/aspects/users/creator.rb 100.00 % 55 25 25 0 4.20 100.00 % 2 2 0
app/aspects/users/updater.rb 100.00 % 49 23 23 0 2.61 100.00 % 4 4 0
app/contract.rb 100.00 % 13 6 6 0 1.00 100.00 % 0 0 0
app/contracts/devices/create.rb 100.00 % 15 7 7 0 1.14 100.00 % 0 0 0
app/contracts/devices/patch.rb 100.00 % 18 9 9 0 1.00 100.00 % 0 0 0
app/contracts/devices/update.rb 100.00 % 18 9 9 0 1.00 100.00 % 0 0 0
app/contracts/extensions/create.rb 100.00 % 19 9 9 0 1.00 100.00 % 0 0 0
app/contracts/extensions/exchanges/create.rb 100.00 % 17 8 8 0 1.00 100.00 % 0 0 0
app/contracts/extensions/exchanges/update.rb 100.00 % 18 9 9 0 1.00 100.00 % 0 0 0
app/contracts/extensions/update.rb 100.00 % 20 10 10 0 1.00 100.00 % 0 0 0
app/contracts/models/clone.rb 100.00 % 19 9 9 0 1.00 100.00 % 0 0 0
app/contracts/models/create.rb 100.00 % 16 7 7 0 1.14 100.00 % 0 0 0
app/contracts/models/update.rb 100.00 % 19 9 9 0 1.00 100.00 % 0 0 0
app/contracts/rules/cron.rb 100.00 % 23 8 8 0 10.25 100.00 % 2 2 0
app/contracts/rules/image_mime_type.rb 100.00 % 14 6 6 0 4.50 100.00 % 2 2 0
app/contracts/rules/sleep_start_at.rb 100.00 % 17 8 8 0 12.38 100.00 % 4 4 0
app/contracts/rules/sleep_stop_at.rb 100.00 % 17 8 8 0 12.25 100.00 % 4 4 0
app/contracts/users/create.rb 100.00 % 24 13 13 0 1.00 100.00 % 0 0 0
app/contracts/users/update.rb 100.00 % 21 11 11 0 1.00 100.00 % 0 0 0
app/db/relation.rb 100.00 % 11 4 4 0 1.00 100.00 % 0 0 0
app/db/repository.rb 100.00 % 11 4 4 0 1.00 100.00 % 0 0 0
app/db/struct.rb 100.00 % 11 4 4 0 1.00 100.00 % 0 0 0
app/jobs/base.rb 100.00 % 17 8 8 0 1.00 100.00 % 0 0 0
app/jobs/batches/extension.rb 100.00 % 38 18 18 0 2.11 100.00 % 4 4 0
app/jobs/extensions/exchange_refresh.rb 100.00 % 26 10 10 0 1.50 100.00 % 2 2 0
app/jobs/extensions/screen.rb 100.00 % 23 10 10 0 2.10 100.00 % 2 2 0
app/jobs/synchronizers/firmware.rb 100.00 % 21 9 9 0 1.56 100.00 % 2 2 0
app/jobs/synchronizers/font.rb 100.00 % 17 7 7 0 1.00 100.00 % 0 0 0
app/jobs/synchronizers/model.rb 100.00 % 28 10 10 0 2.50 100.00 % 2 2 0
app/jobs/synchronizers/sensor.rb 100.00 % 17 7 7 0 1.00 100.00 % 0 0 0
app/providers/logger.rb 100.00 % 54 25 25 0 4.56 100.00 % 3 3 0
app/providers/sidekiq.rb 100.00 % 69 26 26 0 2.12 100.00 % 0 0 0
app/relations/account.rb 100.00 % 12 5 5 0 1.20 100.00 % 0 0 0
app/relations/device.rb 100.00 % 17 9 9 0 1.00 100.00 % 0 0 0
app/relations/device_log.rb 100.00 % 12 5 5 0 1.20 100.00 % 0 0 0
app/relations/device_sensor.rb 100.00 % 12 5 5 0 1.20 100.00 % 0 0 0
app/relations/extension.rb 100.00 % 18 10 10 0 1.00 100.00 % 0 0 0
app/relations/extension_device.rb 100.00 % 15 7 7 0 1.00 100.00 % 0 0 0
app/relations/extension_exchange.rb 100.00 % 12 5 5 0 1.20 100.00 % 0 0 0
app/relations/extension_model.rb 100.00 % 15 7 7 0 1.00 100.00 % 0 0 0
app/relations/firmware.rb 100.00 % 14 6 6 0 14.17 100.00 % 0 0 0
app/relations/membership.rb 100.00 % 15 7 7 0 1.00 100.00 % 0 0 0
app/relations/model.rb 100.00 % 20 12 12 0 1.00 100.00 % 0 0 0
app/relations/model_palette.rb 100.00 % 15 7 7 0 1.00 100.00 % 0 0 0
app/relations/palette.rb 100.00 % 15 7 7 0 1.00 100.00 % 0 0 0
app/relations/playlist.rb 100.00 % 21 9 9 0 1.00 100.00 % 0 0 0
app/relations/playlist_item.rb 100.00 % 26 12 12 0 5.42 100.00 % 0 0 0
app/relations/screen.rb 100.00 % 18 9 9 0 1.00 100.00 % 0 0 0
app/relations/user.rb 100.00 % 15 7 7 0 1.00 100.00 % 0 0 0
app/relations/user_password_hash.rb 100.00 % 10 4 4 0 1.00 100.00 % 0 0 0
app/relations/user_status.rb 100.00 % 10 4 4 0 1.00 100.00 % 0 0 0
app/repositories/account.rb 100.00 % 37 16 16 0 2.00 100.00 % 2 2 0
app/repositories/device.rb 100.00 % 53 24 24 0 7.33 100.00 % 6 6 0
app/repositories/device_log.rb 100.00 % 39 17 17 0 3.29 100.00 % 2 2 0
app/repositories/device_sensor.rb 100.00 % 43 19 19 0 8.26 100.00 % 2 2 0
app/repositories/extension.rb 100.00 % 98 47 47 0 12.89 100.00 % 2 2 0
app/repositories/extension_device.rb 100.00 % 29 12 12 0 2.83 100.00 % 2 2 0
app/repositories/extension_exchange.rb 100.00 % 29 12 12 0 12.92 100.00 % 2 2 0
app/repositories/extension_model.rb 100.00 % 29 12 12 0 2.83 100.00 % 2 2 0
app/repositories/firmware.rb 100.00 % 45 21 21 0 4.62 100.00 % 4 4 0
app/repositories/model.rb 100.00 % 47 21 21 0 15.29 100.00 % 2 2 0
app/repositories/model_palette.rb 100.00 % 29 12 12 0 6.50 100.00 % 2 2 0
app/repositories/palette.rb 100.00 % 37 16 16 0 11.56 100.00 % 2 2 0
app/repositories/playlist.rb 100.00 % 79 38 38 0 9.08 100.00 % 10 10 0
app/repositories/playlist_item.rb 100.00 % 45 20 20 0 9.65 100.00 % 2 2 0
app/repositories/screen.rb 100.00 % 64 32 32 0 11.38 100.00 % 6 6 0
app/repositories/user.rb 100.00 % 39 17 17 0 3.82 100.00 % 2 2 0
app/repositories/user_status.rb 100.00 % 14 6 6 0 1.50 100.00 % 2 2 0
app/schemas/coercers/default_to_array.rb 100.00 % 21 10 10 0 7.90 100.00 % 2 2 0
app/schemas/coercers/default_to_false.rb 100.00 % 22 10 10 0 19.60 100.00 % 4 4 0
app/schemas/coercers/json_to_hash.rb 100.00 % 21 10 10 0 33.40 100.00 % 2 2 0
app/schemas/coercers/lines_to_array.rb 100.00 % 17 7 7 0 9.00 100.00 % 0 0 0
app/schemas/coercers/uri_query_to_hash.rb 100.00 % 20 9 9 0 3.44 100.00 % 2 2 0
app/schemas/devices/patch.rb 100.00 % 30 22 22 0 1.00 100.00 % 0 0 0
app/schemas/devices/sensors/upsert.rb 100.00 % 20 11 11 0 1.00 100.00 % 0 0 0
app/schemas/devices/upsert.rb 100.00 % 32 23 23 0 1.00 100.00 % 0 0 0
app/schemas/extensions/exchanges/upsert.rb 100.00 % 21 11 11 0 1.00 100.00 % 0 0 0
app/schemas/extensions/upsert.rb 100.00 % 36 27 27 0 1.00 100.00 % 0 0 0
app/schemas/firmware/header.rb 100.00 % 25 18 18 0 1.00 100.00 % 0 0 0
app/schemas/models/upsert.rb 100.00 % 28 19 19 0 1.00 100.00 % 0 0 0
app/serializers/device.rb 100.00 % 50 14 14 0 4.00 100.00 % 0 0 0
app/serializers/firmware.rb 100.00 % 30 16 16 0 4.06 100.00 % 2 2 0
app/serializers/model.rb 100.00 % 46 14 14 0 3.57 100.00 % 0 0 0
app/serializers/playlist.rb 100.00 % 43 22 22 0 5.18 100.00 % 2 2 0
app/serializers/playlist_item.rb 100.00 % 27 14 14 0 4.00 100.00 % 0 0 0
app/serializers/screen.rb 100.00 % 37 17 17 0 5.18 100.00 % 2 2 0
app/serializers/transformers/time.rb 100.00 % 17 8 8 0 119.75 100.00 % 3 3 0
app/structs/device.rb 100.00 % 44 19 19 0 5.63 100.00 % 6 6 0
app/structs/device_sensor.rb 100.00 % 16 7 7 0 1.43 100.00 % 0 0 0
app/structs/extension.rb 100.00 % 75 26 26 0 9.42 100.00 % 5 5 0
app/structs/extension_exchange.rb 100.00 % 12 5 5 0 1.00 100.00 % 0 0 0
app/structs/firmware.rb 100.00 % 68 36 36 0 14.47 100.00 % 6 6 0
app/structs/model.rb 100.00 % 18 8 8 0 10.38 100.00 % 2 2 0
app/structs/palette.rb 100.00 % 12 5 5 0 1.00 100.00 % 0 0 0
app/structs/playlist.rb 100.00 % 14 6 6 0 2.67 100.00 % 2 2 0
app/structs/playlist_item.rb 100.00 % 22 5 5 0 2.60 100.00 % 0 0 0
app/structs/screen.rb 100.00 % 85 46 46 0 67.15 100.00 % 6 6 0
app/uploaders/binary.rb 100.00 % 14 6 6 0 3.33 100.00 % 0 0 0
app/uploaders/image.rb 100.00 % 20 9 9 0 30.22 100.00 % 2 2 0
app/view.rb 100.00 % 10 3 3 0 1.00 100.00 % 0 0 0
app/views/bulk/devices/logs/delete.rb 100.00 % 15 6 6 0 1.00 100.00 % 0 0 0
app/views/bulk/firmware/delete.rb 100.00 % 13 5 5 0 1.00 100.00 % 0 0 0
app/views/context.rb 100.00 % 20 8 8 0 47.63 100.00 % 2 2 0
app/views/dashboard/show.rb 100.00 % 31 15 15 0 26.67 100.00 % 0 0 0
app/views/designer/show.rb 100.00 % 13 6 6 0 1.00 100.00 % 0 0 0
app/views/devices/edit.rb 100.00 % 18 10 10 0 1.00 100.00 % 0 0 0
app/views/devices/index.rb 100.00 % 13 6 6 0 1.00 100.00 % 0 0 0
app/views/devices/logs/index.rb 100.00 % 16 8 8 0 1.00 100.00 % 0 0 0
app/views/devices/logs/show.rb 100.00 % 15 7 7 0 1.00 100.00 % 0 0 0
app/views/devices/new.rb 100.00 % 18 10 10 0 1.00 100.00 % 0 0 0
app/views/devices/show.rb 100.00 % 12 5 5 0 1.00 100.00 % 0 0 0
app/views/extensions/build/new.rb 100.00 % 14 6 6 0 1.00 100.00 % 0 0 0
app/views/extensions/clone/new.rb 100.00 % 13 5 5 0 1.00 100.00 % 0 0 0
app/views/extensions/dynamic.rb 100.00 % 14 6 6 0 1.00 100.00 % 0 0 0
app/views/extensions/edit.rb 100.00 % 26 13 13 0 3.31 100.00 % 0 0 0
app/views/extensions/exchanges/edit.rb 100.00 % 19 10 10 0 1.00 100.00 % 0 0 0
app/views/extensions/exchanges/index.rb 100.00 % 15 7 7 0 1.00 100.00 % 0 0 0
app/views/extensions/exchanges/new.rb 100.00 % 19 10 10 0 1.00 100.00 % 0 0 0
app/views/extensions/gallery/index.rb 100.00 % 16 8 8 0 1.00 100.00 % 0 0 0
app/views/extensions/index.rb 100.00 % 13 6 6 0 1.00 100.00 % 0 0 0
app/views/extensions/new.rb 100.00 % 23 11 11 0 2.55 100.00 % 0 0 0
app/views/extensions/sensors/index.rb 100.00 % 14 6 6 0 1.00 100.00 % 0 0 0
app/views/extensions/sources/index.rb 100.00 % 14 6 6 0 1.00 100.00 % 0 0 0
app/views/firmware/edit.rb 100.00 % 16 8 8 0 1.00 100.00 % 0 0 0
app/views/firmware/index.rb 100.00 % 13 6 6 0 1.00 100.00 % 0 0 0
app/views/firmware/new.rb 100.00 % 16 8 8 0 1.00 100.00 % 0 0 0
app/views/firmware/show.rb 100.00 % 12 5 5 0 1.00 100.00 % 0 0 0
app/views/helpers.rb 100.00 % 92 47 47 0 70.68 100.00 % 14 14 0
app/views/models/clone/new.rb 100.00 % 13 5 5 0 1.00 100.00 % 0 0 0
app/views/models/edit.rb 100.00 % 19 10 10 0 1.40 100.00 % 0 0 0
app/views/models/index.rb 100.00 % 13 6 6 0 1.00 100.00 % 0 0 0
app/views/models/new.rb 100.00 % 19 10 10 0 1.70 100.00 % 0 0 0
app/views/models/show.rb 100.00 % 12 5 5 0 1.00 100.00 % 0 0 0
app/views/parts/device.rb 100.00 % 64 42 42 0 6.98 100.00 % 26 26 0
app/views/parts/exchange.rb 100.00 % 46 24 24 0 2.33 100.00 % 2 2 0
app/views/parts/extension.rb 100.00 % 34 17 17 0 2.71 100.00 % 4 4 0
app/views/parts/firmware.rb 100.00 % 19 9 9 0 3.11 100.00 % 2 2 0
app/views/parts/ip_address.rb 100.00 % 18 8 8 0 8.25 100.00 % 2 2 0
app/views/parts/model.rb 100.00 % 44 21 21 0 3.29 100.00 % 6 6 0
app/views/parts/playlist.rb 100.00 % 29 13 13 0 3.31 100.00 % 4 4 0
app/views/parts/screen.rb 100.00 % 16 7 7 0 3.14 100.00 % 4 4 0
app/views/parts/user.rb 100.00 % 23 12 12 0 2.92 100.00 % 4 4 0
app/views/playlists/clone/new.rb 100.00 % 13 5 5 0 1.00 100.00 % 0 0 0
app/views/playlists/edit.rb 100.00 % 17 9 9 0 1.00 100.00 % 0 0 0
app/views/playlists/index.rb 100.00 % 13 6 6 0 1.00 100.00 % 0 0 0
app/views/playlists/items/index.rb 100.00 % 16 8 8 0 1.00 100.00 % 0 0 0
app/views/playlists/items/new.rb 100.00 % 20 11 11 0 1.00 100.00 % 0 0 0
app/views/playlists/items/show.rb 100.00 % 14 6 6 0 1.00 100.00 % 0 0 0
app/views/playlists/mirror/edit.rb 100.00 % 15 7 7 0 1.00 100.00 % 0 0 0
app/views/playlists/new.rb 100.00 % 16 8 8 0 1.00 100.00 % 0 0 0
app/views/playlists/screens/show.rb 100.00 % 46 26 26 0 4.96 100.00 % 10 10 0
app/views/playlists/show.rb 100.00 % 13 6 6 0 1.00 100.00 % 0 0 0
app/views/problem_details/index.rb 100.00 % 11 4 4 0 1.00 100.00 % 0 0 0
app/views/scopes/form_field.rb 100.00 % 37 19 19 0 163.84 100.00 % 8 8 0
app/views/scopes/popover_default_content.rb 100.00 % 14 6 6 0 1.00 100.00 % 0 0 0
app/views/scopes/popover_screen_content.rb 100.00 % 18 8 8 0 1.00 100.00 % 0 0 0
app/views/screens/edit.rb 100.00 % 17 9 9 0 1.00 100.00 % 0 0 0
app/views/screens/gaffe/new.rb 100.00 % 16 7 7 0 1.00 100.00 % 0 0 0
app/views/screens/index.rb 100.00 % 13 6 6 0 1.00 100.00 % 0 0 0
app/views/screens/new.rb 100.00 % 17 9 9 0 1.00 100.00 % 0 0 0
app/views/screens/show.rb 100.00 % 12 5 5 0 1.00 100.00 % 0 0 0
app/views/screens/sleep/new.rb 100.00 % 14 6 6 0 1.00 100.00 % 0 0 0
app/views/screens/welcome/new.rb 100.00 % 16 7 7 0 1.00 100.00 % 0 0 0
app/views/users/edit.rb 100.00 % 17 9 9 0 1.00 100.00 % 0 0 0
app/views/users/index.rb 100.00 % 13 6 6 0 1.00 100.00 % 0 0 0
app/views/users/new.rb 100.00 % 17 9 9 0 1.00 100.00 % 0 0 0
app/views/users/show.rb 100.00 % 12 5 5 0 1.00 100.00 % 0 0 0
config/providers/htmx.rb 100.00 % 15 8 8 0 14.25 100.00 % 2 2 0
config/providers/http.rb 100.00 % 27 11 11 0 1.00 100.00 % 0 0 0
config/providers/liquid.rb 100.00 % 20 10 10 0 5.10 100.00 % 0 0 0
config/providers/logger.rb 100.00 % 5 2 2 0 1.00 100.00 % 0 0 0
config/providers/mini_magick.rb 100.00 % 22 13 13 0 1.08 100.00 % 0 0 0
config/providers/shrine.rb 100.00 % 32 15 15 0 4.13 100.00 % 1 1 0
config/providers/sidekiq.rb 100.00 % 5 2 2 0 1.00 100.00 % 0 0 0
config/providers/trmnl_api.rb 100.00 % 17 9 9 0 1.22 100.00 % 0 0 0
config/routes.rb 100.00 % 193 124 124 0 1.02 100.00 % 0 0 0
lib/terminus/ip_finder.rb 100.00 % 23 12 12 0 46.08 100.00 % 2 2 0
lib/terminus/refines/actions/response.rb 100.00 % 20 10 10 0 20.60 100.00 % 2 2 0
slices/authentication/feature.rb 100.00 % 50 21 21 0 113.71 100.00 % 2 2 0
slices/authentication/middleware.rb 100.00 % 141 74 74 0 24.45 100.00 % 6 6 0
slices/authentication/view.rb 100.00 % 9 3 3 0 1.00 100.00 % 0 0 0
slices/authentication/views/context.rb 100.00 % 11 4 4 0 1.00 100.00 % 0 0 0
slices/health/action.rb 100.00 % 8 2 2 0 1.00 100.00 % 0 0 0
slices/health/actions/show.rb 100.00 % 16 7 7 0 1.00 100.00 % 0 0 0
slices/health/view.rb 100.00 % 8 2 2 0 1.00 100.00 % 0 0 0
slices/health/views/context.rb 100.00 % 11 4 4 0 1.00 100.00 % 0 0 0
slices/health/views/show.rb 100.00 % 10 4 4 0 1.00 100.00 % 0 0 0

Actions ( 100.0% covered at 1.98 hits/line )

108 files in total.
1896 relevant lines, 1896 lines covered and 0 lines missed. ( 100.0% )
255 total branches, 255 branches covered and 0 branches missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
app/actions/api/base.rb 100.00 % 52 27 27 0 11.11 100.00 % 0 0 0
app/actions/api/devices/create.rb 100.00 % 63 25 25 0 1.44 100.00 % 4 4 0
app/actions/api/devices/delete.rb 100.00 % 20 10 10 0 1.20 100.00 % 0 0 0
app/actions/api/devices/index.rb 100.00 % 20 10 10 0 1.30 100.00 % 0 0 0
app/actions/api/devices/patch.rb 100.00 % 44 19 19 0 1.37 100.00 % 2 2 0
app/actions/api/devices/show.rb 100.00 % 25 12 12 0 1.17 100.00 % 2 2 0
app/actions/api/display/show.rb 100.00 % 100 39 39 0 2.46 100.00 % 6 6 0
app/actions/api/firmware/create.rb 100.00 % 84 38 38 0 1.24 100.00 % 2 2 0
app/actions/api/firmware/delete.rb 100.00 % 35 18 18 0 1.11 100.00 % 2 2 0
app/actions/api/firmware/index.rb 100.00 % 20 10 10 0 1.30 100.00 % 0 0 0
app/actions/api/firmware/patch.rb 100.00 % 91 42 42 0 1.40 100.00 % 4 4 0
app/actions/api/firmware/show.rb 100.00 % 25 12 12 0 1.17 100.00 % 2 2 0
app/actions/api/log/create.rb 100.00 % 93 47 47 0 1.96 100.00 % 4 4 0
app/actions/api/models/create.rb 100.00 % 61 34 34 0 1.06 100.00 % 2 2 0
app/actions/api/models/delete.rb 100.00 % 20 10 10 0 1.20 100.00 % 0 0 0
app/actions/api/models/index.rb 100.00 % 20 10 10 0 1.30 100.00 % 0 0 0
app/actions/api/models/patch.rb 100.00 % 63 35 35 0 1.06 100.00 % 2 2 0
app/actions/api/models/show.rb 100.00 % 25 12 12 0 1.17 100.00 % 2 2 0
app/actions/api/playlists/create.rb 100.00 % 63 29 29 0 1.34 100.00 % 2 2 0
app/actions/api/playlists/delete.rb 100.00 % 26 13 13 0 1.15 100.00 % 2 2 0
app/actions/api/playlists/index.rb 100.00 % 20 10 10 0 1.30 100.00 % 0 0 0
app/actions/api/playlists/patch.rb 100.00 % 60 30 30 0 1.33 100.00 % 2 2 0
app/actions/api/playlists/show.rb 100.00 % 25 12 12 0 1.17 100.00 % 2 2 0
app/actions/api/screens/create.rb 100.00 % 95 43 43 0 2.63 100.00 % 8 8 0
app/actions/api/screens/delete.rb 100.00 % 35 18 18 0 1.11 100.00 % 2 2 0
app/actions/api/screens/index.rb 100.00 % 21 10 10 0 1.10 100.00 % 0 0 0
app/actions/api/screens/patch.rb 100.00 % 88 41 41 0 1.85 100.00 % 6 6 0
app/actions/api/setup/show.rb 100.00 % 75 29 29 0 1.76 100.00 % 4 4 0
app/actions/bulk/devices/logs/delete.rb 100.00 % 27 13 13 0 1.23 100.00 % 2 2 0
app/actions/bulk/firmware/delete.rb 100.00 % 19 9 9 0 1.22 100.00 % 0 0 0
app/actions/dashboard/show.rb 100.00 % 20 8 8 0 7.75 100.00 % 0 0 0
app/actions/designer/create.rb 100.00 % 57 26 26 0 2.38 100.00 % 6 6 0
app/actions/designer/show.rb 100.00 % 12 5 5 0 1.00 100.00 % 0 0 0
app/actions/devices/create.rb 100.00 % 47 16 16 0 1.69 100.00 % 4 4 0
app/actions/devices/delete.rb 100.00 % 23 11 11 0 1.64 100.00 % 2 2 0
app/actions/devices/edit.rb 100.00 % 38 13 13 0 1.85 100.00 % 2 2 0
app/actions/devices/index.rb 100.00 % 36 18 18 0 3.44 100.00 % 6 6 0
app/actions/devices/logs/delete.rb 100.00 % 28 14 14 0 1.14 100.00 % 2 2 0
app/actions/devices/logs/index.rb 100.00 % 65 30 30 0 2.40 100.00 % 8 8 0
app/actions/devices/logs/show.rb 100.00 % 31 14 14 0 1.00 100.00 % 0 0 0
app/actions/devices/new.rb 100.00 % 25 7 7 0 1.29 100.00 % 0 0 0
app/actions/devices/show.rb 100.00 % 24 10 10 0 1.90 100.00 % 2 2 0
app/actions/devices/update.rb 100.00 % 51 20 20 0 1.35 100.00 % 4 4 0
app/actions/extensions/build/create.rb 100.00 % 31 16 16 0 1.69 100.00 % 0 0 0
app/actions/extensions/clone/create.rb 100.00 % 55 25 25 0 1.36 100.00 % 4 4 0
app/actions/extensions/clone/new.rb 100.00 % 23 11 11 0 1.09 100.00 % 0 0 0
app/actions/extensions/create.rb 100.00 % 55 25 25 0 1.80 100.00 % 2 2 0
app/actions/extensions/delete.rb 100.00 % 22 11 11 0 1.64 100.00 % 2 2 0
app/actions/extensions/edit.rb 100.00 % 22 10 10 0 2.50 100.00 % 2 2 0
app/actions/extensions/exchanges/create.rb 100.00 % 54 22 22 0 1.36 100.00 % 2 2 0
app/actions/extensions/exchanges/delete.rb 100.00 % 30 15 15 0 1.47 100.00 % 2 2 0
app/actions/extensions/exchanges/edit.rb 100.00 % 41 16 16 0 1.63 100.00 % 2 2 0
app/actions/extensions/exchanges/index.rb 100.00 % 29 12 12 0 2.00 100.00 % 0 0 0
app/actions/extensions/exchanges/new.rb 100.00 % 34 14 14 0 1.79 100.00 % 2 2 0
app/actions/extensions/exchanges/update.rb 100.00 % 56 24 24 0 1.17 100.00 % 2 2 0
app/actions/extensions/export/show.rb 100.00 % 38 18 18 0 1.67 100.00 % 4 4 0
app/actions/extensions/gallery/index.rb 100.00 % 55 30 30 0 2.93 100.00 % 6 6 0
app/actions/extensions/index.rb 100.00 % 34 17 17 0 3.94 100.00 % 6 6 0
app/actions/extensions/new.rb 100.00 % 25 12 12 0 1.50 100.00 % 0 0 0
app/actions/extensions/preview/show.rb 100.00 % 45 21 21 0 2.76 100.00 % 5 5 0
app/actions/extensions/sensors/index.rb 100.00 % 40 19 19 0 4.11 100.00 % 2 2 0
app/actions/extensions/sources/index.rb 100.00 % 37 17 17 0 4.24 100.00 % 0 0 0
app/actions/extensions/update.rb 100.00 % 59 30 30 0 1.23 100.00 % 4 4 0
app/actions/firmware/create.rb 100.00 % 53 24 24 0 1.25 100.00 % 2 2 0
app/actions/firmware/delete.rb 100.00 % 23 11 11 0 2.36 100.00 % 2 2 0
app/actions/firmware/edit.rb 100.00 % 24 10 10 0 2.20 100.00 % 2 2 0
app/actions/firmware/index.rb 100.00 % 32 16 16 0 4.38 100.00 % 4 4 0
app/actions/firmware/new.rb 100.00 % 16 7 7 0 1.29 100.00 % 0 0 0
app/actions/firmware/show.rb 100.00 % 24 10 10 0 2.20 100.00 % 2 2 0
app/actions/firmware/update.rb 100.00 % 63 32 32 0 1.59 100.00 % 6 6 0
app/actions/models/clone/create.rb 100.00 % 42 20 20 0 1.40 100.00 % 4 4 0
app/actions/models/clone/new.rb 100.00 % 21 10 10 0 1.00 100.00 % 0 0 0
app/actions/models/create.rb 100.00 % 41 16 16 0 1.63 100.00 % 2 2 0
app/actions/models/delete.rb 100.00 % 23 11 11 0 1.64 100.00 % 2 2 0
app/actions/models/edit.rb 100.00 % 24 10 10 0 1.90 100.00 % 2 2 0
app/actions/models/index.rb 100.00 % 34 17 17 0 3.94 100.00 % 6 6 0
app/actions/models/new.rb 100.00 % 25 12 12 0 1.50 100.00 % 0 0 0
app/actions/models/show.rb 100.00 % 24 10 10 0 1.60 100.00 % 2 2 0
app/actions/models/update.rb 100.00 % 44 20 20 0 1.35 100.00 % 4 4 0
app/actions/playlists/clone/create.rb 100.00 % 50 25 25 0 1.32 100.00 % 4 4 0
app/actions/playlists/clone/new.rb 100.00 % 21 10 10 0 1.00 100.00 % 0 0 0
app/actions/playlists/create.rb 100.00 % 45 19 19 0 1.53 100.00 % 2 2 0
app/actions/playlists/delete.rb 100.00 % 23 11 11 0 1.64 100.00 % 2 2 0
app/actions/playlists/edit.rb 100.00 % 38 14 14 0 1.93 100.00 % 2 2 0
app/actions/playlists/index.rb 100.00 % 34 17 17 0 4.53 100.00 % 6 6 0
app/actions/playlists/items/create.rb 100.00 % 42 19 19 0 1.37 100.00 % 2 2 0
app/actions/playlists/items/index.rb 100.00 % 21 10 10 0 1.30 100.00 % 0 0 0
app/actions/playlists/mirror/edit.rb 100.00 % 31 11 11 0 2.09 100.00 % 2 2 0
app/actions/playlists/mirror/update.rb 100.00 % 52 22 22 0 2.41 100.00 % 2 2 0
app/actions/playlists/new.rb 100.00 % 16 7 7 0 1.29 100.00 % 0 0 0
app/actions/playlists/screens/index.rb 100.00 % 34 17 17 0 1.53 100.00 % 2 2 0
app/actions/playlists/screens/show.rb 100.00 % 58 24 24 0 3.92 100.00 % 4 4 0
app/actions/playlists/show.rb 100.00 % 38 14 14 0 1.57 100.00 % 2 2 0
app/actions/playlists/update.rb 100.00 % 59 25 25 0 1.28 100.00 % 4 4 0
app/actions/problem_details/index.rb 100.00 % 12 5 5 0 1.00 100.00 % 0 0 0
app/actions/screens/create.rb 100.00 % 59 27 27 0 1.07 100.00 % 2 2 0
app/actions/screens/delete.rb 100.00 % 23 11 11 0 1.64 100.00 % 2 2 0
app/actions/screens/edit.rb 100.00 % 29 10 10 0 2.20 100.00 % 2 2 0
app/actions/screens/index.rb 100.00 % 34 17 17 0 3.35 100.00 % 6 6 0
app/actions/screens/new.rb 100.00 % 16 7 7 0 1.29 100.00 % 0 0 0
app/actions/screens/show.rb 100.00 % 24 10 10 0 2.20 100.00 % 2 2 0
app/actions/screens/update.rb 100.00 % 68 35 35 0 1.54 100.00 % 6 6 0
app/actions/users/create.rb 100.00 % 39 12 12 0 1.42 100.00 % 2 2 0
app/actions/users/edit.rb 100.00 % 29 10 10 0 1.90 100.00 % 2 2 0
app/actions/users/index.rb 100.00 % 34 17 17 0 3.06 100.00 % 6 6 0
app/actions/users/new.rb 100.00 % 16 7 7 0 1.29 100.00 % 0 0 0
app/actions/users/show.rb 100.00 % 24 10 10 0 1.60 100.00 % 2 2 0
app/actions/users/update.rb 100.00 % 39 13 13 0 1.08 100.00 % 2 2 0

Aspects ( 100.0% covered at 9.26 hits/line )

79 files in total.
1612 relevant lines, 1612 lines covered and 0 lines missed. ( 100.0% )
193 total branches, 193 branches covered and 0 branches missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
app/aspects/croner.rb 100.00 % 74 43 43 0 3.79 100.00 % 20 20 0
app/aspects/devices/defaulter.rb 100.00 % 33 12 12 0 10.17 100.00 % 0 0 0
app/aspects/devices/mac_address_builder.rb 100.00 % 20 10 10 0 52.20 100.00 % 0 0 0
app/aspects/devices/provisioner.rb 100.00 % 68 33 33 0 13.06 100.00 % 2 2 0
app/aspects/devices/sensors/synchronizer.rb 100.00 % 91 44 44 0 3.30 100.00 % 6 6 0
app/aspects/devices/synchronizer.rb 100.00 % 34 15 15 0 2.87 100.00 % 2 2 0
app/aspects/downloader.rb 100.00 % 38 20 20 0 5.75 100.00 % 6 6 0
app/aspects/extensions/cloner.rb 100.00 % 77 32 32 0 3.91 100.00 % 0 0 0
app/aspects/extensions/contextualizer.rb 100.00 % 34 14 14 0 9.36 100.00 % 2 2 0
app/aspects/extensions/curler.rb 100.00 % 54 24 24 0 4.58 100.00 % 8 8 0
app/aspects/extensions/defaults.rb 100.00 % 25 4 4 0 1.00 100.00 % 0 0 0
app/aspects/extensions/exchanges/coalescer.rb 100.00 % 19 9 9 0 7.33 100.00 % 0 0 0
app/aspects/extensions/exchanges/refresher.rb 100.00 % 66 33 33 0 3.42 100.00 % 5 5 0
app/aspects/extensions/exporter.rb 100.00 % 27 10 10 0 1.40 100.00 % 0 0 0
app/aspects/extensions/fetchers/input.rb 100.00 % 18 8 8 0 5.75 100.00 % 0 0 0
app/aspects/extensions/fetchers/sole.rb 100.00 % 80 42 42 0 4.79 100.00 % 10 10 0
app/aspects/extensions/importers/remote/creator.rb 100.00 % 83 39 39 0 3.28 100.00 % 5 5 0
app/aspects/extensions/importers/remote/extractor.rb 100.00 % 51 26 26 0 1.62 100.00 % 0 0 0
app/aspects/extensions/importers/remote/schema.rb 100.00 % 30 19 19 0 1.00 100.00 % 0 0 0
app/aspects/extensions/importers/remote/transformer.rb 100.00 % 59 27 27 0 2.59 100.00 % 2 2 0
app/aspects/extensions/importers/remote/transformers/data.rb 100.00 % 37 19 19 0 2.05 100.00 % 2 2 0
app/aspects/extensions/importers/remote/transformers/default.rb 100.00 % 34 15 15 0 1.27 100.00 % 0 0 0
app/aspects/extensions/importers/remote/transformers/keys.rb 100.00 % 43 17 17 0 2.00 100.00 % 0 0 0
app/aspects/extensions/importers/remote/transformers/kind.rb 100.00 % 55 28 28 0 2.82 100.00 % 4 4 0
app/aspects/extensions/importers/remote/transformers/poll.rb 100.00 % 41 19 19 0 2.63 100.00 % 2 2 0
app/aspects/extensions/importers/remote/transformers/template.rb 100.00 % 46 20 20 0 2.55 100.00 % 0 0 0
app/aspects/extensions/importers/remote/transformers/template_keys.rb 100.00 % 58 23 23 0 6.70 100.00 % 0 0 0
app/aspects/extensions/parser.rb 100.00 % 49 27 27 0 2.56 100.00 % 4 4 0
app/aspects/extensions/renderer.rb 100.00 % 40 19 19 0 4.00 100.00 % 4 4 0
app/aspects/extensions/renderers/image.rb 100.00 % 46 20 20 0 1.15 100.00 % 2 2 0
app/aspects/extensions/renderers/poll.rb 100.00 % 42 21 21 0 4.86 100.00 % 0 0 0
app/aspects/extensions/renderers/static.rb 100.00 % 25 11 11 0 1.18 100.00 % 0 0 0
app/aspects/extensions/screen_upserter.rb 100.00 % 27 9 9 0 3.33 100.00 % 0 0 0
app/aspects/extensions/uri_builder.rb 100.00 % 17 8 8 0 6.25 100.00 % 0 0 0
app/aspects/firmware/headers/model.rb 100.00 % 54 15 15 0 6.47 100.00 % 0 0 0
app/aspects/firmware/headers/parser.rb 100.00 % 46 20 20 0 6.75 100.00 % 0 0 0
app/aspects/firmware/headers/transformers/model_name.rb 100.00 % 56 20 20 0 7.60 100.00 % 2 2 0
app/aspects/firmware/headers/transformers/sensors.rb 100.00 % 59 29 29 0 7.69 100.00 % 2 2 0
app/aspects/firmware/log_transformer.rb 100.00 % 24 12 12 0 1.75 100.00 % 0 0 0
app/aspects/firmware/models/setup.rb 100.00 % 27 12 12 0 2.67 100.00 % 0 0 0
app/aspects/firmware/synchronizer.rb 100.00 % 48 24 24 0 2.17 100.00 % 6 6 0
app/aspects/fonts/synchronizer.rb 100.00 % 54 28 28 0 18.46 100.00 % 2 2 0
app/aspects/jobs/schedule.rb 100.00 % 36 18 18 0 5.56 100.00 % 6 6 0
app/aspects/json_formatter.rb 100.00 % 23 12 12 0 15.67 100.00 % 3 3 0
app/aspects/logging/rack_adapter.rb 100.00 % 23 11 11 0 2.00 100.00 % 0 0 0
app/aspects/models/cloner.rb 100.00 % 39 18 18 0 2.44 100.00 % 0 0 0
app/aspects/models/defaults.rb 100.00 % 20 4 4 0 1.00 100.00 % 0 0 0
app/aspects/models/finder.rb 100.00 % 37 16 16 0 23.81 100.00 % 6 6 0
app/aspects/models/palette_optioner.rb 100.00 % 36 15 15 0 5.00 100.00 % 4 4 0
app/aspects/models/synchronizer.rb 100.00 % 85 42 42 0 7.48 100.00 % 8 8 0
app/aspects/palettes/synchronizer.rb 100.00 % 57 31 31 0 2.35 100.00 % 4 4 0
app/aspects/password_encryptor.rb 100.00 % 22 9 9 0 28.11 100.00 % 2 2 0
app/aspects/playlists/cloner.rb 100.00 % 62 30 30 0 3.60 100.00 % 4 4 0
app/aspects/playlists/screen_optioner.rb 100.00 % 18 8 8 0 1.38 100.00 % 0 0 0
app/aspects/playlists/slide_window.rb 100.00 % 36 17 17 0 6.00 100.00 % 4 4 0
app/aspects/problem_detail.rb 100.00 % 60 20 20 0 1.45 100.00 % 0 0 0
app/aspects/sanitizer.rb 100.00 % 37 18 18 0 50.89 100.00 % 0 0 0
app/aspects/screens/converter.rb 100.00 % 14 6 6 0 11.33 100.00 % 2 2 0
app/aspects/screens/converters/color.rb 100.00 % 55 28 28 0 4.21 100.00 % 4 4 0
app/aspects/screens/converters/monochrome.rb 100.00 % 88 53 53 0 25.00 100.00 % 10 10 0
app/aspects/screens/designer/event_stream.rb 100.00 % 57 27 27 0 2.48 100.00 % 2 2 0
app/aspects/screens/designer/middleware.rb 100.00 % 39 14 14 0 88.07 100.00 % 2 2 0
app/aspects/screens/fetcher.rb 100.00 % 47 21 21 0 7.29 100.00 % 6 6 0
app/aspects/screens/find_or_creator.rb 100.00 % 38 18 18 0 9.28 100.00 % 2 2 0
app/aspects/screens/gaffer.rb 100.00 % 18 7 7 0 1.43 100.00 % 0 0 0
app/aspects/screens/mold.rb 100.00 % 50 15 15 0 6.27 100.00 % 2 2 0
app/aspects/screens/mold_builder.rb 100.00 % 51 27 27 0 27.85 100.00 % 2 2 0
app/aspects/screens/placeholder.rb 100.00 % 37 17 17 0 70.18 100.00 % 0 0 0
app/aspects/screens/rotator.rb 100.00 % 52 24 24 0 4.92 100.00 % 8 8 0
app/aspects/screens/shoter.rb 100.00 % 102 55 55 0 23.76 100.00 % 2 2 0
app/aspects/screens/sleeper.rb 100.00 % 18 7 7 0 1.43 100.00 % 0 0 0
app/aspects/screens/temp_pather.rb 100.00 % 35 19 19 0 24.26 100.00 % 2 2 0
app/aspects/screens/upserter.rb 100.00 % 38 17 17 0 6.00 100.00 % 4 4 0
app/aspects/screens/upserters/html.rb 100.00 % 25 13 13 0 4.38 100.00 % 0 0 0
app/aspects/screens/upserters/preprocessed.rb 100.00 % 35 18 18 0 6.78 100.00 % 0 0 0
app/aspects/screens/upserters/unprocessed.rb 100.00 % 47 22 22 0 5.77 100.00 % 0 0 0
app/aspects/screens/welcomer.rb 100.00 % 18 7 7 0 4.57 100.00 % 0 0 0
app/aspects/users/creator.rb 100.00 % 55 25 25 0 4.20 100.00 % 2 2 0
app/aspects/users/updater.rb 100.00 % 49 23 23 0 2.61 100.00 % 4 4 0

Config ( 100.0% covered at 2.03 hits/line )

9 files in total.
194 relevant lines, 194 lines covered and 0 lines missed. ( 100.0% )
3 total branches, 3 branches covered and 0 branches missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
config/providers/htmx.rb 100.00 % 15 8 8 0 14.25 100.00 % 2 2 0
config/providers/http.rb 100.00 % 27 11 11 0 1.00 100.00 % 0 0 0
config/providers/liquid.rb 100.00 % 20 10 10 0 5.10 100.00 % 0 0 0
config/providers/logger.rb 100.00 % 5 2 2 0 1.00 100.00 % 0 0 0
config/providers/mini_magick.rb 100.00 % 22 13 13 0 1.08 100.00 % 0 0 0
config/providers/shrine.rb 100.00 % 32 15 15 0 4.13 100.00 % 1 1 0
config/providers/sidekiq.rb 100.00 % 5 2 2 0 1.00 100.00 % 0 0 0
config/providers/trmnl_api.rb 100.00 % 17 9 9 0 1.22 100.00 % 0 0 0
config/routes.rb 100.00 % 193 124 124 0 1.02 100.00 % 0 0 0

Contracts ( 100.0% covered at 2.99 hits/line )

16 files in total.
140 relevant lines, 140 lines covered and 0 lines missed. ( 100.0% )
12 total branches, 12 branches covered and 0 branches missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
app/contracts/devices/create.rb 100.00 % 15 7 7 0 1.14 100.00 % 0 0 0
app/contracts/devices/patch.rb 100.00 % 18 9 9 0 1.00 100.00 % 0 0 0
app/contracts/devices/update.rb 100.00 % 18 9 9 0 1.00 100.00 % 0 0 0
app/contracts/extensions/create.rb 100.00 % 19 9 9 0 1.00 100.00 % 0 0 0
app/contracts/extensions/exchanges/create.rb 100.00 % 17 8 8 0 1.00 100.00 % 0 0 0
app/contracts/extensions/exchanges/update.rb 100.00 % 18 9 9 0 1.00 100.00 % 0 0 0
app/contracts/extensions/update.rb 100.00 % 20 10 10 0 1.00 100.00 % 0 0 0
app/contracts/models/clone.rb 100.00 % 19 9 9 0 1.00 100.00 % 0 0 0
app/contracts/models/create.rb 100.00 % 16 7 7 0 1.14 100.00 % 0 0 0
app/contracts/models/update.rb 100.00 % 19 9 9 0 1.00 100.00 % 0 0 0
app/contracts/rules/cron.rb 100.00 % 23 8 8 0 10.25 100.00 % 2 2 0
app/contracts/rules/image_mime_type.rb 100.00 % 14 6 6 0 4.50 100.00 % 2 2 0
app/contracts/rules/sleep_start_at.rb 100.00 % 17 8 8 0 12.38 100.00 % 4 4 0
app/contracts/rules/sleep_stop_at.rb 100.00 % 17 8 8 0 12.25 100.00 % 4 4 0
app/contracts/users/create.rb 100.00 % 24 13 13 0 1.00 100.00 % 0 0 0
app/contracts/users/update.rb 100.00 % 21 11 11 0 1.00 100.00 % 0 0 0

DB ( 100.0% covered at 1.0 hits/line )

3 files in total.
12 relevant lines, 12 lines covered and 0 lines missed. ( 100.0% )
0 total branches, 0 branches covered and 0 branches missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
app/db/relation.rb 100.00 % 11 4 4 0 1.00 100.00 % 0 0 0
app/db/repository.rb 100.00 % 11 4 4 0 1.00 100.00 % 0 0 0
app/db/struct.rb 100.00 % 11 4 4 0 1.00 100.00 % 0 0 0

Jobs ( 100.0% covered at 1.71 hits/line )

8 files in total.
79 relevant lines, 79 lines covered and 0 lines missed. ( 100.0% )
12 total branches, 12 branches covered and 0 branches missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
app/jobs/base.rb 100.00 % 17 8 8 0 1.00 100.00 % 0 0 0
app/jobs/batches/extension.rb 100.00 % 38 18 18 0 2.11 100.00 % 4 4 0
app/jobs/extensions/exchange_refresh.rb 100.00 % 26 10 10 0 1.50 100.00 % 2 2 0
app/jobs/extensions/screen.rb 100.00 % 23 10 10 0 2.10 100.00 % 2 2 0
app/jobs/synchronizers/firmware.rb 100.00 % 21 9 9 0 1.56 100.00 % 2 2 0
app/jobs/synchronizers/font.rb 100.00 % 17 7 7 0 1.00 100.00 % 0 0 0
app/jobs/synchronizers/model.rb 100.00 % 28 10 10 0 2.50 100.00 % 2 2 0
app/jobs/synchronizers/sensor.rb 100.00 % 17 7 7 0 1.00 100.00 % 0 0 0

Lib ( 100.0% covered at 34.5 hits/line )

2 files in total.
22 relevant lines, 22 lines covered and 0 lines missed. ( 100.0% )
4 total branches, 4 branches covered and 0 branches missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
lib/terminus/ip_finder.rb 100.00 % 23 12 12 0 46.08 100.00 % 2 2 0
lib/terminus/refines/actions/response.rb 100.00 % 20 10 10 0 20.60 100.00 % 2 2 0

Models ( 100.0% covered at 0.0 hits/line )

0 files in total.
0 relevant lines, 0 lines covered and 0 lines missed. ( 100.0% )
0 total branches, 0 branches covered and 0 branches missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches

Providers ( 100.0% covered at 3.31 hits/line )

2 files in total.
51 relevant lines, 51 lines covered and 0 lines missed. ( 100.0% )
3 total branches, 3 branches covered and 0 branches missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
app/providers/logger.rb 100.00 % 54 25 25 0 4.56 100.00 % 3 3 0
app/providers/sidekiq.rb 100.00 % 69 26 26 0 2.12 100.00 % 0 0 0

Relations ( 100.0% covered at 1.99 hits/line )

19 files in total.
137 relevant lines, 137 lines covered and 0 lines missed. ( 100.0% )
0 total branches, 0 branches covered and 0 branches missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
app/relations/account.rb 100.00 % 12 5 5 0 1.20 100.00 % 0 0 0
app/relations/device.rb 100.00 % 17 9 9 0 1.00 100.00 % 0 0 0
app/relations/device_log.rb 100.00 % 12 5 5 0 1.20 100.00 % 0 0 0
app/relations/device_sensor.rb 100.00 % 12 5 5 0 1.20 100.00 % 0 0 0
app/relations/extension.rb 100.00 % 18 10 10 0 1.00 100.00 % 0 0 0
app/relations/extension_device.rb 100.00 % 15 7 7 0 1.00 100.00 % 0 0 0
app/relations/extension_exchange.rb 100.00 % 12 5 5 0 1.20 100.00 % 0 0 0
app/relations/extension_model.rb 100.00 % 15 7 7 0 1.00 100.00 % 0 0 0
app/relations/firmware.rb 100.00 % 14 6 6 0 14.17 100.00 % 0 0 0
app/relations/membership.rb 100.00 % 15 7 7 0 1.00 100.00 % 0 0 0
app/relations/model.rb 100.00 % 20 12 12 0 1.00 100.00 % 0 0 0
app/relations/model_palette.rb 100.00 % 15 7 7 0 1.00 100.00 % 0 0 0
app/relations/palette.rb 100.00 % 15 7 7 0 1.00 100.00 % 0 0 0
app/relations/playlist.rb 100.00 % 21 9 9 0 1.00 100.00 % 0 0 0
app/relations/playlist_item.rb 100.00 % 26 12 12 0 5.42 100.00 % 0 0 0
app/relations/screen.rb 100.00 % 18 9 9 0 1.00 100.00 % 0 0 0
app/relations/user.rb 100.00 % 15 7 7 0 1.00 100.00 % 0 0 0
app/relations/user_password_hash.rb 100.00 % 10 4 4 0 1.00 100.00 % 0 0 0
app/relations/user_status.rb 100.00 % 10 4 4 0 1.00 100.00 % 0 0 0

Repositories ( 100.0% covered at 8.5 hits/line )

17 files in total.
342 relevant lines, 342 lines covered and 0 lines missed. ( 100.0% )
52 total branches, 52 branches covered and 0 branches missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
app/repositories/account.rb 100.00 % 37 16 16 0 2.00 100.00 % 2 2 0
app/repositories/device.rb 100.00 % 53 24 24 0 7.33 100.00 % 6 6 0
app/repositories/device_log.rb 100.00 % 39 17 17 0 3.29 100.00 % 2 2 0
app/repositories/device_sensor.rb 100.00 % 43 19 19 0 8.26 100.00 % 2 2 0
app/repositories/extension.rb 100.00 % 98 47 47 0 12.89 100.00 % 2 2 0
app/repositories/extension_device.rb 100.00 % 29 12 12 0 2.83 100.00 % 2 2 0
app/repositories/extension_exchange.rb 100.00 % 29 12 12 0 12.92 100.00 % 2 2 0
app/repositories/extension_model.rb 100.00 % 29 12 12 0 2.83 100.00 % 2 2 0
app/repositories/firmware.rb 100.00 % 45 21 21 0 4.62 100.00 % 4 4 0
app/repositories/model.rb 100.00 % 47 21 21 0 15.29 100.00 % 2 2 0
app/repositories/model_palette.rb 100.00 % 29 12 12 0 6.50 100.00 % 2 2 0
app/repositories/palette.rb 100.00 % 37 16 16 0 11.56 100.00 % 2 2 0
app/repositories/playlist.rb 100.00 % 79 38 38 0 9.08 100.00 % 10 10 0
app/repositories/playlist_item.rb 100.00 % 45 20 20 0 9.65 100.00 % 2 2 0
app/repositories/screen.rb 100.00 % 64 32 32 0 11.38 100.00 % 6 6 0
app/repositories/user.rb 100.00 % 39 17 17 0 3.82 100.00 % 2 2 0
app/repositories/user_status.rb 100.00 % 14 6 6 0 1.50 100.00 % 2 2 0

Schemas ( 100.0% covered at 4.71 hits/line )

12 files in total.
177 relevant lines, 177 lines covered and 0 lines missed. ( 100.0% )
10 total branches, 10 branches covered and 0 branches missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
app/schemas/coercers/default_to_array.rb 100.00 % 21 10 10 0 7.90 100.00 % 2 2 0
app/schemas/coercers/default_to_false.rb 100.00 % 22 10 10 0 19.60 100.00 % 4 4 0
app/schemas/coercers/json_to_hash.rb 100.00 % 21 10 10 0 33.40 100.00 % 2 2 0
app/schemas/coercers/lines_to_array.rb 100.00 % 17 7 7 0 9.00 100.00 % 0 0 0
app/schemas/coercers/uri_query_to_hash.rb 100.00 % 20 9 9 0 3.44 100.00 % 2 2 0
app/schemas/devices/patch.rb 100.00 % 30 22 22 0 1.00 100.00 % 0 0 0
app/schemas/devices/sensors/upsert.rb 100.00 % 20 11 11 0 1.00 100.00 % 0 0 0
app/schemas/devices/upsert.rb 100.00 % 32 23 23 0 1.00 100.00 % 0 0 0
app/schemas/extensions/exchanges/upsert.rb 100.00 % 21 11 11 0 1.00 100.00 % 0 0 0
app/schemas/extensions/upsert.rb 100.00 % 36 27 27 0 1.00 100.00 % 0 0 0
app/schemas/firmware/header.rb 100.00 % 25 18 18 0 1.00 100.00 % 0 0 0
app/schemas/models/upsert.rb 100.00 % 28 19 19 0 1.00 100.00 % 0 0 0

Serializers ( 100.0% covered at 13.21 hits/line )

7 files in total.
105 relevant lines, 105 lines covered and 0 lines missed. ( 100.0% )
9 total branches, 9 branches covered and 0 branches missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
app/serializers/device.rb 100.00 % 50 14 14 0 4.00 100.00 % 0 0 0
app/serializers/firmware.rb 100.00 % 30 16 16 0 4.06 100.00 % 2 2 0
app/serializers/model.rb 100.00 % 46 14 14 0 3.57 100.00 % 0 0 0
app/serializers/playlist.rb 100.00 % 43 22 22 0 5.18 100.00 % 2 2 0
app/serializers/playlist_item.rb 100.00 % 27 14 14 0 4.00 100.00 % 0 0 0
app/serializers/screen.rb 100.00 % 37 17 17 0 5.18 100.00 % 2 2 0
app/serializers/transformers/time.rb 100.00 % 17 8 8 0 119.75 100.00 % 3 3 0

Slices ( 100.0% covered at 34.9 hits/line )

9 files in total.
121 relevant lines, 121 lines covered and 0 lines missed. ( 100.0% )
8 total branches, 8 branches covered and 0 branches missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
slices/authentication/feature.rb 100.00 % 50 21 21 0 113.71 100.00 % 2 2 0
slices/authentication/middleware.rb 100.00 % 141 74 74 0 24.45 100.00 % 6 6 0
slices/authentication/view.rb 100.00 % 9 3 3 0 1.00 100.00 % 0 0 0
slices/authentication/views/context.rb 100.00 % 11 4 4 0 1.00 100.00 % 0 0 0
slices/health/action.rb 100.00 % 8 2 2 0 1.00 100.00 % 0 0 0
slices/health/actions/show.rb 100.00 % 16 7 7 0 1.00 100.00 % 0 0 0
slices/health/view.rb 100.00 % 8 2 2 0 1.00 100.00 % 0 0 0
slices/health/views/context.rb 100.00 % 11 4 4 0 1.00 100.00 % 0 0 0
slices/health/views/show.rb 100.00 % 10 4 4 0 1.00 100.00 % 0 0 0

Structs ( 100.0% covered at 25.12 hits/line )

10 files in total.
163 relevant lines, 163 lines covered and 0 lines missed. ( 100.0% )
27 total branches, 27 branches covered and 0 branches missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
app/structs/device.rb 100.00 % 44 19 19 0 5.63 100.00 % 6 6 0
app/structs/device_sensor.rb 100.00 % 16 7 7 0 1.43 100.00 % 0 0 0
app/structs/extension.rb 100.00 % 75 26 26 0 9.42 100.00 % 5 5 0
app/structs/extension_exchange.rb 100.00 % 12 5 5 0 1.00 100.00 % 0 0 0
app/structs/firmware.rb 100.00 % 68 36 36 0 14.47 100.00 % 6 6 0
app/structs/model.rb 100.00 % 18 8 8 0 10.38 100.00 % 2 2 0
app/structs/palette.rb 100.00 % 12 5 5 0 1.00 100.00 % 0 0 0
app/structs/playlist.rb 100.00 % 14 6 6 0 2.67 100.00 % 2 2 0
app/structs/playlist_item.rb 100.00 % 22 5 5 0 2.60 100.00 % 0 0 0
app/structs/screen.rb 100.00 % 85 46 46 0 67.15 100.00 % 6 6 0

Uploaders ( 100.0% covered at 19.47 hits/line )

2 files in total.
15 relevant lines, 15 lines covered and 0 lines missed. ( 100.0% )
2 total branches, 2 branches covered and 0 branches missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
app/uploaders/binary.rb 100.00 % 14 6 6 0 3.33 100.00 % 0 0 0
app/uploaders/image.rb 100.00 % 20 9 9 0 30.22 100.00 % 2 2 0

Views ( 100.0% covered at 12.99 hits/line )

67 files in total.
650 relevant lines, 650 lines covered and 0 lines missed. ( 100.0% )
88 total branches, 88 branches covered and 0 branches missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
app/views/bulk/devices/logs/delete.rb 100.00 % 15 6 6 0 1.00 100.00 % 0 0 0
app/views/bulk/firmware/delete.rb 100.00 % 13 5 5 0 1.00 100.00 % 0 0 0
app/views/context.rb 100.00 % 20 8 8 0 47.63 100.00 % 2 2 0
app/views/dashboard/show.rb 100.00 % 31 15 15 0 26.67 100.00 % 0 0 0
app/views/designer/show.rb 100.00 % 13 6 6 0 1.00 100.00 % 0 0 0
app/views/devices/edit.rb 100.00 % 18 10 10 0 1.00 100.00 % 0 0 0
app/views/devices/index.rb 100.00 % 13 6 6 0 1.00 100.00 % 0 0 0
app/views/devices/logs/index.rb 100.00 % 16 8 8 0 1.00 100.00 % 0 0 0
app/views/devices/logs/show.rb 100.00 % 15 7 7 0 1.00 100.00 % 0 0 0
app/views/devices/new.rb 100.00 % 18 10 10 0 1.00 100.00 % 0 0 0
app/views/devices/show.rb 100.00 % 12 5 5 0 1.00 100.00 % 0 0 0
app/views/extensions/build/new.rb 100.00 % 14 6 6 0 1.00 100.00 % 0 0 0
app/views/extensions/clone/new.rb 100.00 % 13 5 5 0 1.00 100.00 % 0 0 0
app/views/extensions/dynamic.rb 100.00 % 14 6 6 0 1.00 100.00 % 0 0 0
app/views/extensions/edit.rb 100.00 % 26 13 13 0 3.31 100.00 % 0 0 0
app/views/extensions/exchanges/edit.rb 100.00 % 19 10 10 0 1.00 100.00 % 0 0 0
app/views/extensions/exchanges/index.rb 100.00 % 15 7 7 0 1.00 100.00 % 0 0 0
app/views/extensions/exchanges/new.rb 100.00 % 19 10 10 0 1.00 100.00 % 0 0 0
app/views/extensions/gallery/index.rb 100.00 % 16 8 8 0 1.00 100.00 % 0 0 0
app/views/extensions/index.rb 100.00 % 13 6 6 0 1.00 100.00 % 0 0 0
app/views/extensions/new.rb 100.00 % 23 11 11 0 2.55 100.00 % 0 0 0
app/views/extensions/sensors/index.rb 100.00 % 14 6 6 0 1.00 100.00 % 0 0 0
app/views/extensions/sources/index.rb 100.00 % 14 6 6 0 1.00 100.00 % 0 0 0
app/views/firmware/edit.rb 100.00 % 16 8 8 0 1.00 100.00 % 0 0 0
app/views/firmware/index.rb 100.00 % 13 6 6 0 1.00 100.00 % 0 0 0
app/views/firmware/new.rb 100.00 % 16 8 8 0 1.00 100.00 % 0 0 0
app/views/firmware/show.rb 100.00 % 12 5 5 0 1.00 100.00 % 0 0 0
app/views/helpers.rb 100.00 % 92 47 47 0 70.68 100.00 % 14 14 0
app/views/models/clone/new.rb 100.00 % 13 5 5 0 1.00 100.00 % 0 0 0
app/views/models/edit.rb 100.00 % 19 10 10 0 1.40 100.00 % 0 0 0
app/views/models/index.rb 100.00 % 13 6 6 0 1.00 100.00 % 0 0 0
app/views/models/new.rb 100.00 % 19 10 10 0 1.70 100.00 % 0 0 0
app/views/models/show.rb 100.00 % 12 5 5 0 1.00 100.00 % 0 0 0
app/views/parts/device.rb 100.00 % 64 42 42 0 6.98 100.00 % 26 26 0
app/views/parts/exchange.rb 100.00 % 46 24 24 0 2.33 100.00 % 2 2 0
app/views/parts/extension.rb 100.00 % 34 17 17 0 2.71 100.00 % 4 4 0
app/views/parts/firmware.rb 100.00 % 19 9 9 0 3.11 100.00 % 2 2 0
app/views/parts/ip_address.rb 100.00 % 18 8 8 0 8.25 100.00 % 2 2 0
app/views/parts/model.rb 100.00 % 44 21 21 0 3.29 100.00 % 6 6 0
app/views/parts/playlist.rb 100.00 % 29 13 13 0 3.31 100.00 % 4 4 0
app/views/parts/screen.rb 100.00 % 16 7 7 0 3.14 100.00 % 4 4 0
app/views/parts/user.rb 100.00 % 23 12 12 0 2.92 100.00 % 4 4 0
app/views/playlists/clone/new.rb 100.00 % 13 5 5 0 1.00 100.00 % 0 0 0
app/views/playlists/edit.rb 100.00 % 17 9 9 0 1.00 100.00 % 0 0 0
app/views/playlists/index.rb 100.00 % 13 6 6 0 1.00 100.00 % 0 0 0
app/views/playlists/items/index.rb 100.00 % 16 8 8 0 1.00 100.00 % 0 0 0
app/views/playlists/items/new.rb 100.00 % 20 11 11 0 1.00 100.00 % 0 0 0
app/views/playlists/items/show.rb 100.00 % 14 6 6 0 1.00 100.00 % 0 0 0
app/views/playlists/mirror/edit.rb 100.00 % 15 7 7 0 1.00 100.00 % 0 0 0
app/views/playlists/new.rb 100.00 % 16 8 8 0 1.00 100.00 % 0 0 0
app/views/playlists/screens/show.rb 100.00 % 46 26 26 0 4.96 100.00 % 10 10 0
app/views/playlists/show.rb 100.00 % 13 6 6 0 1.00 100.00 % 0 0 0
app/views/problem_details/index.rb 100.00 % 11 4 4 0 1.00 100.00 % 0 0 0
app/views/scopes/form_field.rb 100.00 % 37 19 19 0 163.84 100.00 % 8 8 0
app/views/scopes/popover_default_content.rb 100.00 % 14 6 6 0 1.00 100.00 % 0 0 0
app/views/scopes/popover_screen_content.rb 100.00 % 18 8 8 0 1.00 100.00 % 0 0 0
app/views/screens/edit.rb 100.00 % 17 9 9 0 1.00 100.00 % 0 0 0
app/views/screens/gaffe/new.rb 100.00 % 16 7 7 0 1.00 100.00 % 0 0 0
app/views/screens/index.rb 100.00 % 13 6 6 0 1.00 100.00 % 0 0 0
app/views/screens/new.rb 100.00 % 17 9 9 0 1.00 100.00 % 0 0 0
app/views/screens/show.rb 100.00 % 12 5 5 0 1.00 100.00 % 0 0 0
app/views/screens/sleep/new.rb 100.00 % 14 6 6 0 1.00 100.00 % 0 0 0
app/views/screens/welcome/new.rb 100.00 % 16 7 7 0 1.00 100.00 % 0 0 0
app/views/users/edit.rb 100.00 % 17 9 9 0 1.00 100.00 % 0 0 0
app/views/users/index.rb 100.00 % 13 6 6 0 1.00 100.00 % 0 0 0
app/views/users/new.rb 100.00 % 17 9 9 0 1.00 100.00 % 0 0 0
app/views/users/show.rb 100.00 % 12 5 5 0 1.00 100.00 % 0 0 0

Ungrouped ( 100.0% covered at 105.76 hits/line )

3 files in total.
25 relevant lines, 25 lines covered and 0 lines missed. ( 100.0% )
2 total branches, 2 branches covered and 0 branches missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
app/action.rb 100.00 % 44 16 16 0 164.69 100.00 % 2 2 0
app/contract.rb 100.00 % 13 6 6 0 1.00 100.00 % 0 0 0
app/view.rb 100.00 % 10 3 3 0 1.00 100.00 % 0 0 0

app/action.rb

100.0% lines covered

100.0% branches covered

16 relevant lines. 16 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 require "dry/monads"
  4. 1 require "hanami/action"
  5. 1 require "initable"
  6. 1 module Terminus
  7. # The application base action.
  8. 1 class Action < Hanami::Action
  9. 1 include Dry::Monads[:result]
  10. 1 before :authorize
  11. 1 protected
  12. 1 def authorize request, response
  13. 465 rodauth = request.env["rodauth"]
  14. 465 else: 279 then: 186 return unless rodauth
  15. 558 handle_rodauth_redirect(rodauth, response) { rodauth.require_account }
  16. 274 response[:current_user_id] = rodauth.account_id
  17. end
  18. 1 private
  19. 1 def handle_rodauth_redirect rodauth, response
  20. 558 halted = catch(:halt) { yield }
  21. skipped # :nocov:
  22. skipped return unless halted
  23. skipped
  24. skipped code, headers, body = *halted
  25. skipped
  26. skipped rodauth.flash.next.each { |key, value| response.flash[key] = value }
  27. skipped response.redirect headers["Location"], code
  28. skipped
  29. skipped throw :halt, [code, body]
  30. skipped # :nocov:
  31. end
  32. end
  33. end

app/actions/api/base.rb

100.0% lines covered

100.0% branches covered

27 relevant lines. 27 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "petail"
  3. 1 require_relative "../../aspects/problem_detail"
  4. 1 module Terminus
  5. 1 module Actions
  6. 1 module API
  7. # The base action.
  8. 1 class Base < Action
  9. 1 config.formats.accept :json
  10. 1 handle_exception Dry::Types::SchemaError => :detail_enum,
  11. ROM::SQL::UniqueConstraintError => :detail_duplicate,
  12. ROM::SQL::ForeignKeyConstraintError => :detail_foreign_key
  13. 1 using Refines::Actions::Response
  14. 1 def initialize(problem: Petail, problem_detail: Aspects::ProblemDetail, **)
  15. 92 @problem = problem
  16. 92 @problem_detail = problem_detail
  17. 92 super(**)
  18. end
  19. 1 protected
  20. 1 attr_reader :problem
  21. 1 def verify_csrf_token?(*) = false
  22. 1 private
  23. 1 attr_reader :problem_detail
  24. 1 def detail_duplicate request, response, error
  25. 1 payload = problem_detail.duplicate error.message, request.path
  26. 1 response.with body: payload.to_json, format: :problem_details, status: payload.status
  27. end
  28. 1 def detail_enum request, response, error
  29. 1 payload = problem_detail.enum error.message, request.path
  30. 1 response.with body: payload.to_json, format: :problem_details, status: payload.status
  31. end
  32. 1 def detail_foreign_key request, response, error
  33. 1 payload = problem_detail.foreign_key error.message, request.path
  34. 1 response.with body: payload.to_json, format: :problem_details, status: payload.status
  35. end
  36. end
  37. end
  38. end
  39. end

app/actions/api/devices/create.rb

100.0% lines covered

100.0% branches covered

25 relevant lines. 25 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module API
  5. 1 module Devices
  6. # The create action.
  7. 1 class Create < Base
  8. 1 include Deps["aspects.devices.provisioner"]
  9. 1 include Initable[serializer: Serializers::Device]
  10. 1 using Refines::Actions::Response
  11. 1 contract Contracts::Devices::Create
  12. 1 def handle request, response
  13. 4 parameters = request.params
  14. 4 then: 3 if parameters.valid?
  15. 3 process parameters, response
  16. else: 1 else
  17. 1 unprocessable_content parameters, response
  18. end
  19. end
  20. 1 private
  21. 1 def process parameters, response
  22. 3 in: 2 case provisioner.call(**parameters[:device])
  23. 2 in: 1 in Success(device) then response.body = {data: serializer.new(device).to_h}.to_json
  24. 1 in Failure(String => error) then not_found error, response
  25. skipped # :nocov:
  26. skipped # :nocov:
  27. end
  28. end
  29. 1 def not_found error, response
  30. 1 payload = problem[
  31. type: "/problem_details#device_payload",
  32. status: __method__,
  33. detail: error,
  34. instance: "/api/devices"
  35. ]
  36. 1 response.with body: payload.to_json, format: :problem_details, status: payload.status
  37. end
  38. 1 def unprocessable_content parameters, response
  39. 1 payload = problem[
  40. type: "/problem_details#device_payload",
  41. status: :unprocessable_content,
  42. detail: "Validation failed.",
  43. instance: "/api/devices",
  44. extensions: {errors: parameters.errors.to_h}
  45. ]
  46. 1 response.with body: payload.to_json, format: :problem_details, status: payload.status
  47. end
  48. end
  49. end
  50. end
  51. end
  52. end

app/actions/api/devices/delete.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module API
  5. 1 module Devices
  6. # The delete action.
  7. 1 class Delete < Base
  8. 1 include Deps[repository: "repositories.device"]
  9. 1 include Initable[serializer: Serializers::Device]
  10. 1 def handle request, response
  11. 2 device = repository.delete request.params[:id]
  12. 2 response.body = {data: serializer.new(device).to_h}.to_json
  13. end
  14. end
  15. end
  16. end
  17. end
  18. end

app/actions/api/devices/index.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module API
  5. 1 module Devices
  6. # The index action.
  7. 1 class Index < Base
  8. 1 include Deps[repository: "repositories.device"]
  9. 1 include Initable[serializer: Serializers::Device]
  10. 1 def handle *, response
  11. 3 data = repository.all.map { serializer.new(it).to_h }
  12. 2 response.body = {data:}.to_json
  13. end
  14. end
  15. end
  16. end
  17. end
  18. end

app/actions/api/devices/patch.rb

100.0% lines covered

100.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module API
  5. 1 module Devices
  6. # The patch action.
  7. 1 class Patch < Base
  8. 1 include Deps[repository: "repositories.device"]
  9. 1 include Initable[serializer: Serializers::Device]
  10. 1 using Refines::Actions::Response
  11. 1 contract Contracts::Devices::Patch
  12. 1 def handle request, response
  13. 3 parameters = request.params
  14. 3 then: 1 if parameters.valid?
  15. 1 device = repository.update(*parameters.to_h.values_at(:id, :device))
  16. 1 response.body = {data: serializer.new(device).to_h}.to_json
  17. else: 2 else
  18. 2 unprocessable_content parameters, response
  19. end
  20. end
  21. 1 private
  22. 1 def unprocessable_content parameters, response
  23. 2 payload = problem[
  24. type: "/problem_details#device_payload",
  25. status: :unprocessable_content,
  26. detail: "Validation failed.",
  27. instance: "/api/devices",
  28. extensions: {errors: parameters.errors.to_h}
  29. ]
  30. 2 response.with body: payload.to_json, format: :problem_details, status: payload.status
  31. end
  32. end
  33. end
  34. end
  35. end
  36. end

app/actions/api/devices/show.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module API
  5. 1 module Devices
  6. # The show action.
  7. 1 class Show < Base
  8. 1 include Deps[repository: "repositories.device"]
  9. 1 include Initable[serializer: Serializers::Device]
  10. 1 def handle request, response
  11. 2 device = repository.find request.params[:id]
  12. 2 then: 1 response.body = if device
  13. 1 {data: serializer.new(device).to_h}.to_json
  14. else: 1 else
  15. 1 problem[status: :not_found].to_json
  16. end
  17. end
  18. end
  19. end
  20. end
  21. end
  22. end

app/actions/api/display/show.rb

100.0% lines covered

100.0% branches covered

39 relevant lines. 39 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "trmnl/api"
  3. 1 module Terminus
  4. 1 module Actions
  5. 1 module API
  6. 1 module Display
  7. # The show action.
  8. # :reek:DataClump
  9. 1 class Show < Base
  10. 1 include Deps[
  11. :settings,
  12. "aspects.devices.synchronizer",
  13. "aspects.screens.rotator",
  14. "aspects.screens.gaffer",
  15. firmware_repository: "repositories.firmware"
  16. ]
  17. 1 include Initable[model: TRMNL::API::Models::Display]
  18. 1 using Refines::Actions::Response
  19. 1 def handle request, response
  20. 9 in: 6 case synchronizer.call request.env
  21. 6 else: 3 in Success(device) then rotate device, response
  22. 3 else not_found response
  23. end
  24. end
  25. 1 protected
  26. 1 def authorize(*) = nil
  27. 1 private
  28. 1 def rotate device, response
  29. 6 rotator.call(device)
  30. 4 .either -> screen { success device, screen, response },
  31. 2 -> message { error_for device, message, response }
  32. end
  33. 1 def success device, screen, response
  34. attributes = {
  35. 4 filename: screen.image_name_with_checksum,
  36. image_url: screen.image_uri(host: settings.api_uri)
  37. }
  38. 4 response.body = build_payload(device, attributes).to_json
  39. end
  40. 1 def error_for device, message, response
  41. 4 gaffer.call(device, message).bind { |screen| any_error device, screen, response }
  42. end
  43. 1 def build_payload device, attributes
  44. 4 model[**fetch_firmware(device), **attributes, **device.as_api_display]
  45. end
  46. 1 def fetch_firmware device
  47. 6 firmware_repository.latest.then do |firmware|
  48. 6 else: 3 then: 3 break unless firmware
  49. 3 version = firmware.version
  50. 3 then: 1 else: 2 break if device.firmware_version == version
  51. {
  52. 2 firmware_url: firmware.attachment_uri(host: settings.api_uri),
  53. firmware_version: version
  54. }
  55. end
  56. end
  57. 1 def any_error device, screen, response
  58. 2 payload = model[
  59. filename: screen.image_name,
  60. image_url: screen.image_uri(host: settings.api_uri),
  61. **fetch_firmware(device),
  62. **device.as_api_display
  63. ]
  64. 2 response.body = payload.to_json
  65. end
  66. 1 def not_found response
  67. 3 payload = problem[
  68. type: "/problem_details#device_id",
  69. status: __method__,
  70. detail: "Invalid device ID.",
  71. instance: "/api/display"
  72. ]
  73. 3 response.with body: payload.to_json, format: :problem_details, status: payload.status
  74. end
  75. end
  76. end
  77. end
  78. end
  79. end

app/actions/api/firmware/create.rb

100.0% lines covered

100.0% branches covered

38 relevant lines. 38 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "refinements/pathname"
  3. 1 module Terminus
  4. 1 module Actions
  5. 1 module API
  6. 1 module Firmware
  7. # The create action.
  8. 1 class Create < Base
  9. 1 include Deps["aspects.downloader", repository: "repositories.firmware"]
  10. 1 include Initable[serializer: Serializers::Firmware]
  11. 1 using Refines::Actions::Response
  12. 1 using Refinements::Pathname
  13. 1 params do
  14. 1 required(:firmware).filled :hash do
  15. 1 required(:version).filled Types::Version
  16. 1 required(:kind).filled :string
  17. 1 required(:uri).filled :string
  18. end
  19. end
  20. 1 def handle request, response
  21. 3 parameters = request.params
  22. 3 then: 2 if parameters.valid?
  23. 2 process parameters[:firmware], response
  24. else: 1 else
  25. 1 unprocessable_content parameters, response
  26. end
  27. end
  28. 1 private
  29. 1 def process parameters, response
  30. 2 uri = parameters.delete :uri
  31. 2 record = repository.create parameters
  32. 2 downloader.call(uri)
  33. 1 .either -> http_response { upload record, http_response.body.to_s, response },
  34. 1 proc { unprocessable_download uri, response }
  35. end
  36. # :reek:FeatureEnvy
  37. # :reek:TooManyStatements
  38. 1 def upload record, content, response
  39. 1 Pathname.mktmpdir do |root|
  40. 2 root.join("#{record.version}.bin").write(content).open { record.upload it }
  41. end
  42. 1 update = repository.update record.id, attachment_data: record.attachment_attributes
  43. 1 response.body = {data: serializer.new(update).to_h}.to_json
  44. end
  45. 1 def unprocessable_download uri, response
  46. 1 payload = problem[
  47. type: "/problem_details#firmware_payload",
  48. status: :unprocessable_content,
  49. detail: "Invalid URI: #{uri}.",
  50. instance: "/api/firmware"
  51. ]
  52. 1 response.with body: payload.to_json, format: :problem_details, status: payload.status
  53. end
  54. 1 def unprocessable_content parameters, response
  55. 1 payload = problem[
  56. type: "/problem_details#firmware_payload",
  57. status: :unprocessable_content,
  58. detail: "Validation failed.",
  59. instance: "/api/firmware",
  60. extensions: {errors: parameters.errors.to_h}
  61. ]
  62. 1 response.with body: payload.to_json, format: :problem_details, status: payload.status
  63. end
  64. end
  65. end
  66. end
  67. end
  68. end

app/actions/api/firmware/delete.rb

100.0% lines covered

100.0% branches covered

18 relevant lines. 18 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module API
  5. 1 module Firmware
  6. # The delete action.
  7. 1 class Delete < Base
  8. 1 include Deps[repository: "repositories.firmware"]
  9. 1 include Initable[serializer: Serializers::Firmware]
  10. 1 using Refines::Actions::Response
  11. 1 def handle request, response
  12. 2 repository.find(request.params[:id]).then do |firmware|
  13. 2 then: 1 else: 1 firmware ? success(firmware, response) : failure(response)
  14. end
  15. end
  16. 1 private
  17. 1 def success firmware, response
  18. 1 repository.delete firmware.id
  19. 1 response.body = {data: serializer.new(firmware).to_h}.to_json
  20. end
  21. 1 def failure response
  22. 1 payload = problem[status: :not_found]
  23. 1 response.with body: payload.to_json, format: :problem_details, status: payload.status
  24. end
  25. end
  26. end
  27. end
  28. end
  29. end

app/actions/api/firmware/index.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module API
  5. 1 module Firmware
  6. # The index action.
  7. 1 class Index < Base
  8. 1 include Deps[repository: "repositories.firmware"]
  9. 1 include Initable[serializer: Serializers::Firmware]
  10. 1 def handle *, response
  11. 3 data = repository.all.map { serializer.new(it).to_h }
  12. 2 response.body = {data:}.to_json
  13. end
  14. end
  15. end
  16. end
  17. end
  18. end

app/actions/api/firmware/patch.rb

100.0% lines covered

100.0% branches covered

42 relevant lines. 42 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "refinements/pathname"
  3. 1 module Terminus
  4. 1 module Actions
  5. 1 module API
  6. 1 module Firmware
  7. # The patch action.
  8. 1 class Patch < Base
  9. 1 include Deps["aspects.downloader", repository: "repositories.firmware"]
  10. 1 include Initable[serializer: Serializers::Firmware]
  11. 1 using Refines::Actions::Response
  12. 1 using Refinements::Pathname
  13. 1 params do
  14. 1 required(:id).filled :integer
  15. 1 required(:firmware).filled :hash do
  16. 1 optional(:version).filled Types::Version
  17. 1 optional(:kind).filled :string
  18. 1 optional(:uri).filled :string
  19. end
  20. end
  21. 1 def handle request, response
  22. 4 parameters = request.params
  23. 4 then: 3 if parameters.valid?
  24. 3 process(*parameters.to_h.values_at(:id, :firmware), response)
  25. else: 1 else
  26. 1 unprocessable_content parameters, response
  27. end
  28. end
  29. 1 private
  30. 1 def process id, parameters, response
  31. 3 uri = parameters.delete :uri
  32. 3 record = repository.update id, parameters
  33. 3 else: 2 then: 1 return response.with body: {data: serializer.new(record).to_h}.to_json unless uri
  34. 2 download uri, record, response
  35. end
  36. 1 def download uri, record, response
  37. 2 downloader.call(uri)
  38. 1 .either -> payload { replace record, payload.body.to_s, response },
  39. 1 proc { unprocessable_download uri, response }
  40. end
  41. # :reek:FeatureEnvy
  42. 1 def replace record, content, response
  43. 1 Pathname.mktmpdir do |root|
  44. 2 root.join("#{record.version}.bin").write(content).open { record.replace it }
  45. end
  46. 1 update = repository.update record.id, attachment_data: record.attachment_attributes
  47. 1 response.with body: {data: serializer.new(update).to_h}.to_json
  48. end
  49. 1 def unprocessable_download uri, response
  50. 1 payload = problem[
  51. type: "/problem_details#firmware_payload",
  52. status: :unprocessable_content,
  53. detail: "Invalid URI: #{uri}.",
  54. instance: "/api/firmware"
  55. ]
  56. 1 response.with body: payload.to_json, format: :problem_details, status: payload.status
  57. end
  58. 1 def unprocessable_content parameters, response
  59. 1 payload = problem[
  60. type: "/problem_details#firmware_payload",
  61. status: :unprocessable_content,
  62. detail: "Validation failed.",
  63. instance: "/api/firmware",
  64. extensions: {errors: parameters.errors.to_h}
  65. ]
  66. 1 response.with body: payload.to_json, format: :problem_details, status: payload.status
  67. end
  68. end
  69. end
  70. end
  71. end
  72. end

app/actions/api/firmware/show.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module API
  5. 1 module Firmware
  6. # The show action.
  7. 1 class Show < Base
  8. 1 include Deps[repository: "repositories.firmware"]
  9. 1 include Initable[serializer: Serializers::Firmware]
  10. 1 def handle request, response
  11. 2 firmware = repository.find request.params[:id]
  12. 2 then: 1 response.body = if firmware
  13. 1 {data: serializer.new(firmware).to_h}.to_json
  14. else: 1 else
  15. 1 problem[status: :not_found].to_json
  16. end
  17. end
  18. end
  19. end
  20. end
  21. end
  22. end

app/actions/api/log/create.rb

100.0% lines covered

100.0% branches covered

47 relevant lines. 47 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module API
  5. 1 module Log
  6. # The create action.
  7. 1 class Create < Base
  8. 1 include Deps[
  9. :logger,
  10. transformer: "aspects.firmware.log_transformer",
  11. device_repository: "repositories.device",
  12. log_repository: "repositories.device_log"
  13. ]
  14. 1 using Refines::Actions::Response
  15. 1 params do
  16. 1 required(:logs).filled(:array).each(:hash) do
  17. 1 required(:battery_voltage).filled :float
  18. 1 required(:created_at).filled :integer
  19. 1 required(:firmware_version).filled :string
  20. 1 required(:free_heap_size).filled :integer
  21. 1 required(:max_alloc_size).filled :integer
  22. 1 required(:id).filled :integer
  23. 1 required(:message).filled :string
  24. 1 required(:refresh_rate).filled :integer
  25. 1 optional(:retry).filled :integer
  26. 1 required(:sleep_duration).filled :integer
  27. 1 required(:source_line).filled :integer
  28. 1 required(:source_path).filled :string
  29. 1 required(:special_function).filled :string
  30. 1 required(:wake_reason).filled :string
  31. 1 required(:wifi_signal).filled :integer
  32. 1 required(:wifi_status).filled :string
  33. end
  34. end
  35. 1 def handle request, response
  36. 8 parameters = request.params
  37. 8 device = device_repository.find_by mac_address: request.get_header("HTTP_ID")
  38. 8 else: 6 then: 2 return not_found response unless device
  39. 6 else: 2 then: 4 return unprocessable_content parameters, response unless parameters.valid?
  40. 2 save device, parameters, response
  41. end
  42. 1 protected
  43. 1 def authorize(*) = nil
  44. 1 private
  45. 1 def save device, parameters, response
  46. 2 transformer.call(parameters.to_h).each do |attributes|
  47. 2 log_repository.create attributes.merge!(device_id: device.id)
  48. end
  49. 2 response.status = 204
  50. end
  51. 1 def not_found response
  52. 2 payload = problem[
  53. type: "/problem_details#device_id",
  54. status: __method__,
  55. detail: "Invalid device ID.",
  56. instance: "/api/log"
  57. ]
  58. 2 logger.error "Unable to find device."
  59. 2 response.with body: payload.to_json, format: :problem_details, status: payload.status
  60. end
  61. 1 def unprocessable_content parameters, response
  62. 4 errors = parameters.errors.to_h
  63. 4 payload = problem[
  64. type: "/problem_details#log_payload",
  65. status: __method__,
  66. detail: "Validation failed due to incorrect or invalid payload.",
  67. instance: "/api/log",
  68. extensions: {errors:}
  69. ]
  70. 4 logger.error errors
  71. 4 response.with body: payload.to_json, format: :problem_details, status: payload.status
  72. end
  73. end
  74. end
  75. end
  76. end
  77. end

app/actions/api/models/create.rb

100.0% lines covered

100.0% branches covered

34 relevant lines. 34 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module API
  5. 1 module Models
  6. # The create action.
  7. 1 class Create < Base
  8. 1 include Deps[repository: "repositories.model"]
  9. 1 include Initable[serializer: Serializers::Model]
  10. 1 using Refines::Actions::Response
  11. 1 params do
  12. 1 required(:model).filled(:hash) do
  13. 1 optional(:default_palette_id).maybe :integer
  14. 1 required(:name).filled :string
  15. 1 required(:label).filled :string
  16. 1 optional(:description).maybe :string
  17. 1 optional(:mime_type).filled :string
  18. 1 optional(:bit_depth).filled :integer
  19. 1 optional(:colors).filled :integer
  20. 1 optional(:scale_factor).filled :float
  21. 1 optional(:rotation).filled :integer
  22. 1 optional(:offset_x).filled :integer
  23. 1 optional(:offset_y).filled :integer
  24. 1 optional(:css).maybe :hash
  25. 1 optional(:width).filled :integer
  26. 1 optional(:height).filled :integer
  27. end
  28. end
  29. 1 def handle request, response
  30. 2 parameters = request.params
  31. 2 then: 1 if parameters.valid?
  32. 1 model = repository.create parameters[:model]
  33. 1 response.body = {data: serializer.new(model).to_h}.to_json
  34. else: 1 else
  35. 1 unprocessable_content parameters, response
  36. end
  37. end
  38. 1 private
  39. 1 def unprocessable_content parameters, response
  40. 1 payload = problem[
  41. type: "/problem_details#model_payload",
  42. status: :unprocessable_content,
  43. detail: "Validation failed.",
  44. instance: "/api/models",
  45. extensions: {errors: parameters.errors.to_h}
  46. ]
  47. 1 response.with body: payload.to_json, format: :problem_details, status: payload.status
  48. end
  49. end
  50. end
  51. end
  52. end
  53. end

app/actions/api/models/delete.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module API
  5. 1 module Models
  6. # The delete action.
  7. 1 class Delete < Base
  8. 1 include Deps[repository: "repositories.model"]
  9. 1 include Initable[serializer: Serializers::Model]
  10. 1 def handle request, response
  11. 2 model = repository.delete request.params[:id]
  12. 2 response.body = {data: serializer.new(model).to_h}.to_json
  13. end
  14. end
  15. end
  16. end
  17. end
  18. end

app/actions/api/models/index.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module API
  5. 1 module Models
  6. # The index action.
  7. 1 class Index < Base
  8. 1 include Deps[repository: "repositories.model"]
  9. 1 include Initable[serializer: Serializers::Model]
  10. 1 def handle *, response
  11. 3 data = repository.all.map { serializer.new(it).to_h }
  12. 2 response.body = {data:}.to_json
  13. end
  14. end
  15. end
  16. end
  17. end
  18. end

app/actions/api/models/patch.rb

100.0% lines covered

100.0% branches covered

35 relevant lines. 35 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module API
  5. 1 module Models
  6. # The patch action.
  7. 1 class Patch < Base
  8. 1 include Deps[repository: "repositories.model"]
  9. 1 include Initable[serializer: Serializers::Model]
  10. 1 using Refines::Actions::Response
  11. 1 params do
  12. 1 required(:id).filled :integer
  13. 1 required(:model).filled(:hash) do
  14. 1 optional(:default_palette_id).maybe :integer
  15. 1 optional(:name).filled :string
  16. 1 optional(:label).filled :string
  17. 1 optional(:description).maybe :string
  18. 1 optional(:mime_type).filled :string
  19. 1 optional(:bit_depth).filled :integer
  20. 1 optional(:colors).filled :integer
  21. 1 optional(:scale_factor).filled :float
  22. 1 optional(:rotation).filled :integer
  23. 1 optional(:offset_x).filled :integer
  24. 1 optional(:offset_y).filled :integer
  25. 1 optional(:css).maybe :hash
  26. 1 optional(:width).filled :integer
  27. 1 optional(:height).filled :integer
  28. end
  29. end
  30. 1 def handle request, response
  31. 2 parameters = request.params
  32. 2 then: 1 if parameters.valid?
  33. 1 model = repository.update(*parameters.to_h.values_at(:id, :model))
  34. 1 response.body = {data: serializer.new(model).to_h}.to_json
  35. else: 1 else
  36. 1 unprocessable_content parameters, response
  37. end
  38. end
  39. 1 private
  40. 1 def unprocessable_content parameters, response
  41. 1 payload = problem[
  42. type: "/problem_details#model_payload",
  43. status: :unprocessable_content,
  44. detail: "Validation failed.",
  45. instance: "/api/models",
  46. extensions: {errors: parameters.errors.to_h}
  47. ]
  48. 1 response.with body: payload.to_json, format: :problem_details, status: payload.status
  49. end
  50. end
  51. end
  52. end
  53. end
  54. end

app/actions/api/models/show.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module API
  5. 1 module Models
  6. # The show action.
  7. 1 class Show < Base
  8. 1 include Deps[repository: "repositories.model"]
  9. 1 include Initable[serializer: Serializers::Model]
  10. 1 def handle request, response
  11. 2 model = repository.find request.params[:id]
  12. 2 then: 1 response.body = if model
  13. 1 {data: serializer.new(model).to_h}.to_json
  14. else: 1 else
  15. 1 problem[status: :not_found].to_json
  16. end
  17. end
  18. end
  19. end
  20. end
  21. end
  22. end

app/actions/api/playlists/create.rb

100.0% lines covered

100.0% branches covered

29 relevant lines. 29 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 module Terminus
  4. 1 module Actions
  5. 1 module API
  6. 1 module Playlists
  7. # The create action.
  8. 1 class Create < Base
  9. 1 include Deps[
  10. repository: "repositories.playlist",
  11. item_repository: "repositories.playlist_item"
  12. ]
  13. 1 include Initable[serializer: Serializers::Playlist]
  14. 1 using Refines::Actions::Response
  15. 1 params do
  16. 1 required(:playlist).filled(:hash) do
  17. 1 required(:name).filled :string
  18. 1 required(:label).filled :string
  19. 1 optional(:mode).filled :string
  20. 2 optional(:items).maybe(:array).each(:hash) { required(:screen_id).filled :integer }
  21. end
  22. end
  23. 1 def handle request, response
  24. 3 parameters = request.params
  25. 3 then: 2 if parameters.valid?
  26. 2 playlist = save parameters[:playlist]
  27. 2 response.body = {data: serializer.new(playlist).to_h}.to_json
  28. else: 1 else
  29. 1 unprocessable_content parameters, response
  30. end
  31. end
  32. 1 private
  33. 1 def save attributes
  34. 2 items = attributes.fetch :items, Core::EMPTY_ARRAY
  35. 2 playlist = repository.create_with_items attributes, items
  36. 2 repository.with_items.by_pk(playlist.id).one
  37. end
  38. 1 def unprocessable_content parameters, response
  39. 1 payload = problem[
  40. type: "/problem_details#playlist_payload",
  41. status: :unprocessable_content,
  42. detail: "Validation failed.",
  43. instance: "/api/playlists",
  44. extensions: {errors: parameters.errors.to_h}
  45. ]
  46. 1 response.with body: payload.to_json, format: :problem_details, status: payload.status
  47. end
  48. end
  49. end
  50. end
  51. end
  52. end

app/actions/api/playlists/delete.rb

100.0% lines covered

100.0% branches covered

13 relevant lines. 13 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module API
  5. 1 module Playlists
  6. # The delete action.
  7. 1 class Delete < Base
  8. 1 include Deps[repository: "repositories.playlist"]
  9. 1 include Initable[serializer: Serializers::Playlist]
  10. 1 def handle request, response
  11. 2 playlist = repository.with_items.by_pk(request.params[:id]).one
  12. 2 then: 1 response.body = if playlist
  13. 1 repository.delete playlist.id
  14. 1 {data: serializer.new(playlist).to_h}.to_json
  15. else: 1 else
  16. 1 {data: {}}.to_json
  17. end
  18. end
  19. end
  20. end
  21. end
  22. end
  23. end

app/actions/api/playlists/index.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module API
  5. 1 module Playlists
  6. # The index action.
  7. 1 class Index < Base
  8. 1 include Deps[repository: "repositories.playlist"]
  9. 1 include Initable[serializer: Serializers::Playlist]
  10. 1 def handle *, response
  11. 3 data = repository.with_items.to_a.map { serializer.new(it).to_h }
  12. 2 response.body = {data:}.to_json
  13. end
  14. end
  15. end
  16. end
  17. end
  18. end

app/actions/api/playlists/patch.rb

100.0% lines covered

100.0% branches covered

30 relevant lines. 30 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module API
  5. 1 module Playlists
  6. # The patch action.
  7. 1 class Patch < Base
  8. 1 include Deps[repository: "repositories.playlist"]
  9. 1 include Initable[serializer: Serializers::Playlist]
  10. 1 using Refines::Actions::Response
  11. 1 params do
  12. 1 required(:id).filled :integer
  13. 1 required(:playlist).filled(:hash) do
  14. 1 optional(:current_item_id).filled :integer
  15. 1 required(:name).filled :string
  16. 1 required(:label).filled :string
  17. 1 optional(:mode).filled :string
  18. 2 optional(:items).maybe(:array).each(:hash) { required(:screen_id).filled :integer }
  19. end
  20. end
  21. 1 def handle request, response
  22. 3 parameters = request.params
  23. 3 then: 2 if parameters.valid?
  24. 2 playlist = update parameters
  25. 2 response.body = {data: serializer.new(playlist).to_h}.to_json
  26. else: 1 else
  27. 1 unprocessable_content parameters, response
  28. end
  29. end
  30. 1 private
  31. 1 def update parameters
  32. 2 id, attributes = parameters.to_h.values_at :id, :playlist
  33. 2 repository.update_with_items id, attributes, attributes[:items]
  34. 2 repository.with_items.by_pk(id).one
  35. end
  36. 1 def unprocessable_content parameters, response
  37. 1 payload = problem[
  38. type: "/problem_details#playlist_payload",
  39. status: :unprocessable_content,
  40. detail: "Validation failed.",
  41. instance: "/api/playlists",
  42. extensions: {errors: parameters.errors.to_h}
  43. ]
  44. 1 response.with body: payload.to_json, format: :problem_details, status: payload.status
  45. end
  46. end
  47. end
  48. end
  49. end
  50. end

app/actions/api/playlists/show.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module API
  5. 1 module Playlists
  6. # The show action.
  7. 1 class Show < Base
  8. 1 include Deps[repository: "repositories.playlist"]
  9. 1 include Initable[serializer: Serializers::Playlist]
  10. 1 def handle request, response
  11. 2 playlist = repository.with_items.by_pk(request.params[:id]).one
  12. 2 then: 1 response.body = if playlist
  13. 1 {data: serializer.new(playlist).to_h}.to_json
  14. else: 1 else
  15. 1 problem[status: :not_found].to_json
  16. end
  17. end
  18. end
  19. end
  20. end
  21. end
  22. end

app/actions/api/screens/create.rb

100.0% lines covered

100.0% branches covered

43 relevant lines. 43 lines covered and 0 lines missed.
8 total branches, 8 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module API
  5. 1 module Screens
  6. # The create action.
  7. 1 class Create < Base
  8. 1 include Deps[
  9. "aspects.screens.upserter",
  10. repository: "repositories.screen",
  11. playlist_item_repository: "repositories.playlist_item"
  12. ]
  13. 1 include Initable[serializer: Serializers::Screen]
  14. 1 using Refines::Actions::Response
  15. 1 params do
  16. 1 required(:screen).filled(:hash) do
  17. 1 optional(:playlist_id).filled :integer
  18. 1 required(:model_id).filled :integer
  19. 1 required(:label).filled :string
  20. 1 required(:name).filled :string
  21. 1 optional(:mode).filled :string
  22. 1 optional(:content).filled :string
  23. 1 optional(:uri).filled :string
  24. 1 optional(:preprocessed).filled :bool
  25. end
  26. end
  27. 1 def handle request, response
  28. 9 parameters = request.params
  29. 9 then: 7 if parameters.valid?
  30. 7 save parameters, response
  31. else: 2 else
  32. 2 unprocessable_content_for_parameters parameters.errors.to_h, response
  33. end
  34. end
  35. 1 private
  36. 1 def save parameters, response
  37. 13 result = find(parameters).bind { upserter.call(**parameters[:screen]) }
  38. 7 case result
  39. in: 4 in Success(screen)
  40. 4 create_playlist_item parameters.dig(:screen, :playlist_id), screen
  41. 4 else: 3 response.body = {data: serializer.new(screen).to_h}.to_json
  42. 3 else unprocessable_content_for_creation result, response
  43. end
  44. end
  45. 1 def find parameters
  46. 7 model_id, name = parameters[:screen].to_h.values_at :model_id, :name
  47. 7 else: 1 then: 6 return Success() unless repository.find_by(model_id:, name:)
  48. 1 Failure "Screen exists with name (#{name.inspect}) and model ID (#{model_id})."
  49. end
  50. 1 def create_playlist_item playlist_id, screen
  51. 4 else: 1 then: 3 return unless playlist_id
  52. 1 playlist_item_repository.create_with_position playlist_id:, screen_id: screen.id
  53. end
  54. 1 def unprocessable_content_for_parameters errors, response
  55. 2 payload = problem[
  56. type: "/problem_details#screen_payload",
  57. status: :unprocessable_content,
  58. detail: "Validation failed.",
  59. instance: "/api/screens",
  60. extensions: {errors:}
  61. ]
  62. 2 response.with body: payload.to_json, format: :problem_details, status: payload.status
  63. end
  64. 1 def unprocessable_content_for_creation result, response
  65. 3 payload = problem[
  66. type: "/problem_details#screen_payload",
  67. status: :unprocessable_content,
  68. detail: result.failure,
  69. instance: "/api/screens"
  70. ]
  71. 3 response.with body: payload.to_json, format: :problem_details, status: payload.status
  72. end
  73. end
  74. end
  75. end
  76. end
  77. end

app/actions/api/screens/delete.rb

100.0% lines covered

100.0% branches covered

18 relevant lines. 18 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module API
  5. 1 module Screens
  6. # The delete action.
  7. 1 class Delete < Base
  8. 1 include Deps[:settings, repository: "repositories.screen"]
  9. 1 include Initable[serializer: Serializers::Screen]
  10. 1 using Refines::Actions::Response
  11. 1 def handle request, response
  12. 2 repository.find(request.params[:id]).then do |screen|
  13. 2 then: 1 else: 1 screen ? success(screen, response) : failure(response)
  14. end
  15. end
  16. 1 private
  17. 1 def success screen, response
  18. 1 repository.delete screen.id
  19. 1 response.body = {data: serializer.new(screen).to_h}.to_json
  20. end
  21. 1 def failure response
  22. 1 payload = problem[status: :not_found]
  23. 1 response.with body: payload.to_json, format: :problem_details, status: payload.status
  24. end
  25. end
  26. end
  27. end
  28. end
  29. end

app/actions/api/screens/index.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module API
  5. 1 module Screens
  6. # The index action.
  7. 1 class Index < Base
  8. 1 include Deps[repository: "repositories.screen"]
  9. 1 include Initable[serializer: Serializers::Screen]
  10. 1 def handle(*, response) = response.body = {data:}.to_json
  11. 1 private
  12. 2 def data = repository.all.map { serializer.new(it).to_h }
  13. end
  14. end
  15. end
  16. end
  17. end

app/actions/api/screens/patch.rb

100.0% lines covered

100.0% branches covered

41 relevant lines. 41 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module API
  5. 1 module Screens
  6. # The patch action.
  7. # :reek:DataClump
  8. 1 class Patch < Base
  9. 1 include Deps["aspects.screens.upserter", repository: "repositories.screen"]
  10. 1 include Initable[serializer: Serializers::Screen]
  11. 1 using Refines::Actions::Response
  12. 1 params do
  13. 1 required(:id).filled(:integer)
  14. 1 required(:screen).filled(:hash) do
  15. 1 optional(:model_id).filled :integer
  16. 1 optional(:label).filled :string
  17. 1 optional(:name).filled :string
  18. 1 optional(:mode).filled :string
  19. 1 optional(:content).filled :string
  20. 1 optional(:uri).filled :string
  21. 1 optional(:preprocessed).filled :bool
  22. end
  23. end
  24. 1 def handle request, response
  25. 5 parameters = request.params
  26. 5 then: 4 if parameters.valid?
  27. 4 save parameters, response
  28. else: 1 else
  29. 1 unprocessable_content_for_parameters parameters.errors.to_h, response
  30. end
  31. end
  32. 1 private
  33. 1 def save parameters, response
  34. 7 result = find(parameters).bind { |record| update record, parameters }
  35. 4 case result
  36. in: 2 in Success(screen)
  37. 2 else: 2 response.body = {data: serializer.new(screen).to_h}.to_json
  38. 2 else unprocessable_content_for_creation result, response
  39. end
  40. end
  41. 1 def find parameters
  42. 4 id = parameters[:id]
  43. 4 record = repository.find id
  44. 4 then: 3 else: 1 record ? Success(record) : Failure("Unable to find screen: #{id}.")
  45. end
  46. 1 def update record, parameters
  47. 3 upserter.call(**record.to_h.slice(:model_id, :name, :label), **parameters[:screen])
  48. end
  49. 1 def unprocessable_content_for_parameters errors, response
  50. 1 payload = problem[
  51. type: "/problem_details#screen_payload",
  52. status: :unprocessable_content,
  53. detail: "Validation failed.",
  54. instance: "/api/screens",
  55. extensions: {errors:}
  56. ]
  57. 1 response.with body: payload.to_json, format: :problem_details, status: payload.status
  58. end
  59. 1 def unprocessable_content_for_creation result, response
  60. 2 payload = problem[
  61. type: "/problem_details#screen_payload",
  62. status: :unprocessable_content,
  63. detail: result.failure,
  64. instance: "/api/screens"
  65. ]
  66. 2 response.with body: payload.to_json, format: :problem_details, status: payload.status
  67. end
  68. end
  69. end
  70. end
  71. end
  72. end

app/actions/api/setup/show.rb

100.0% lines covered

100.0% branches covered

29 relevant lines. 29 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module API
  5. 1 module Setup
  6. # The show action.
  7. 1 class Show < Base
  8. 1 include Deps[
  9. "aspects.devices.provisioner",
  10. firmware_parser: "aspects.firmware.headers.parser",
  11. model_repository: "repositories.model"
  12. ]
  13. 1 include Initable[payload: Aspects::Firmware::Models::Setup]
  14. 1 using Refines::Actions::Response
  15. 1 def handle request, response
  16. 6 in: 3 case firmware_parser.call request.env
  17. 3 in: 3 in Success(model) then create model, response
  18. 3 in Failure(result) then unprocessable_content result.errors.to_h, response
  19. skipped # :nocov:
  20. skipped # :nocov:
  21. end
  22. end
  23. 1 protected
  24. 1 def authorize(*) = nil
  25. 1 private
  26. 1 def create model, response
  27. 3 firmware_version, mac_address, model_name = model.to_h.values_at :firmware_version,
  28. :mac_address,
  29. :model_name
  30. 3 provisioner.call(model_id: find_model_id(model_name), mac_address:, firmware_version:)
  31. 2 .either -> device { render_success device, response },
  32. 1 -> error { not_found error, response }
  33. end
  34. 4 then: 2 else: 1 def find_model_id(name) = model_repository.find_by(name:).then { it.id if it }
  35. 1 def render_success device, response
  36. 2 response.body = payload.for(device).to_json
  37. end
  38. 1 def not_found error, response
  39. 1 payload = problem[
  40. type: "/problem_details#device_setup",
  41. status: __method__,
  42. detail: error,
  43. instance: "/api/setup"
  44. ]
  45. 1 response.with body: payload.to_json, format: :problem_details, status: payload.status
  46. end
  47. 1 def unprocessable_content errors, response
  48. 3 payload = problem[
  49. type: "/problem_details#device_setup",
  50. status: __method__,
  51. detail: "Invalid request headers.",
  52. instance: "/api/setup",
  53. extensions: {errors:}
  54. ]
  55. 3 response.with body: payload.to_json, format: :problem_details, status: payload.status
  56. end
  57. end
  58. end
  59. end
  60. end
  61. end

app/actions/bulk/devices/logs/delete.rb

100.0% lines covered

100.0% branches covered

13 relevant lines. 13 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Bulk
  5. 1 module Devices
  6. 1 module Logs
  7. # The delete action.
  8. 1 class Delete < Action
  9. 1 include Deps[repository: "repositories.device_log"]
  10. 2 params { required(:device_id).filled :integer }
  11. 1 def handle request, response
  12. 2 parameters = request.params
  13. 2 else: 1 then: 1 halt :unprocessable_content unless parameters.valid?
  14. 1 repository.delete_all_by_device parameters[:device_id]
  15. 1 response.render view, layout: false
  16. end
  17. end
  18. end
  19. end
  20. end
  21. end
  22. end

app/actions/bulk/firmware/delete.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Bulk
  5. 1 module Firmware
  6. # The delete action.
  7. 1 class Delete < Action
  8. 1 include Deps[repository: "repositories.firmware"]
  9. 1 def handle _request, response
  10. 2 repository.delete_all
  11. 2 response.render view, layout: false
  12. end
  13. end
  14. end
  15. end
  16. end
  17. end

app/actions/dashboard/show.rb

100.0% lines covered

100.0% branches covered

8 relevant lines. 8 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Dashboard
  5. # The show action.
  6. 1 class Show < Action
  7. 1 include Deps[:settings, firmware_repository: "repositories.firmware"]
  8. 1 include Initable[ip_finder: proc { Terminus::IPFinder.new }]
  9. 1 def handle _request, response
  10. 55 response.render view,
  11. api_uri: settings.api_uri,
  12. firmware: firmware_repository.latest,
  13. ip_addresses: ip_finder.all
  14. end
  15. end
  16. end
  17. end
  18. end

app/actions/designer/create.rb

100.0% lines covered

100.0% branches covered

26 relevant lines. 26 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Designer
  5. # The create action.
  6. 1 class Create < Action
  7. 1 include Deps[
  8. :htmx,
  9. "aspects.screens.upserter",
  10. model_repository: "repositories.model",
  11. screen_repository: "repositories.screen",
  12. show_view: "views.designer.show"
  13. ]
  14. 1 using Refines::Actions::Response
  15. 1 params do
  16. 1 required(:template).filled(:hash) do
  17. 1 required(:name).filled :string
  18. 1 required(:label).filled :string
  19. 1 required(:content).filled :string
  20. end
  21. end
  22. 1 def handle request, response
  23. 6 parameters = request.params
  24. 6 else: 5 then: 1 halt 422 unless parameters.valid?
  25. 5 then: 4 if htmx.request? request.env, :request, "true"
  26. 4 render_text parameters[:template], response
  27. else: 1 else
  28. 1 response.render show_view, id: Time.new.utc.to_i
  29. end
  30. end
  31. 1 private
  32. 1 def render_text template, response
  33. 4 name, label, content = template.values_at :name, :label, :content
  34. 4 rebuild_screen name, label, content
  35. 4 response.with body: content.strip, status: 201
  36. end
  37. 1 def rebuild_screen name, label, content
  38. 8 then: 1 else: 3 screen_repository.find_by(name:).then { screen_repository.delete it.id if it }
  39. 4 upserter.call model_id: load_model.id, name:, label:, content:
  40. end
  41. # FIX: Use dynamic lookup once the UI support picking the correct model.
  42. 1 def load_model = model_repository.find_by name: "og_png"
  43. end
  44. end
  45. end
  46. end

app/actions/designer/show.rb

100.0% lines covered

100.0% branches covered

5 relevant lines. 5 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Designer
  5. # The show action.
  6. 1 class Show < Action
  7. 1 def handle(*, response) = response.render view
  8. end
  9. end
  10. end
  11. end

app/actions/devices/create.rb

100.0% lines covered

100.0% branches covered

16 relevant lines. 16 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Devices
  5. # The create action.
  6. 1 class Create < Action
  7. 1 include Deps[
  8. :htmx_layout,
  9. "aspects.devices.provisioner",
  10. repository: "repositories.device",
  11. model_repository: "repositories.model",
  12. playlist_repository: "repositories.playlist",
  13. index_view: "views.devices.index"
  14. ]
  15. 1 contract Contracts::Devices::Create
  16. 1 def handle request, response
  17. 4 parameters = request.params
  18. 4 case provision parameters
  19. in: 3 in Success
  20. 3 else: 1 response.render index_view, devices: repository.all, layout: htmx_layout.call(request)
  21. 1 else error response, parameters
  22. end
  23. end
  24. 1 private
  25. 1 def provision parameters
  26. 4 then: 3 else: 1 parameters.valid? ? provisioner.call(**parameters[:device]) : Failure
  27. end
  28. 1 def error response, parameters
  29. 1 response.render view,
  30. models: model_repository.all,
  31. playlists: playlist_repository.all,
  32. device: nil,
  33. fields: parameters[:device],
  34. errors: parameters.errors[:device],
  35. layout: false
  36. end
  37. end
  38. end
  39. end
  40. end

app/actions/devices/delete.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Devices
  5. # The delete action.
  6. 1 class Delete < Action
  7. 1 include Deps[repository: "repositories.device"]
  8. 2 params { required(:id).filled :integer }
  9. 1 def handle request, response
  10. 3 parameters = request.params
  11. 3 else: 2 then: 1 halt :unprocessable_content unless parameters.valid?
  12. 2 repository.delete parameters[:id]
  13. 2 response.body = ""
  14. end
  15. end
  16. end
  17. end
  18. end

app/actions/devices/edit.rb

100.0% lines covered

100.0% branches covered

13 relevant lines. 13 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Devices
  5. # The edit action.
  6. 1 class Edit < Action
  7. 1 include Deps[
  8. :htmx_layout,
  9. repository: "repositories.device",
  10. model_repository: "repositories.model",
  11. playlist_repository: "repositories.playlist"
  12. ]
  13. 2 params { required(:id).filled :integer }
  14. 1 def handle request, response
  15. 4 parameters = request.params
  16. 4 else: 3 then: 1 halt :unprocessable_content unless parameters.valid?
  17. 3 response.render view, **view_settings(request, parameters)
  18. end
  19. 1 private
  20. 1 def view_settings request, parameters
  21. {
  22. 3 models: model_repository.all,
  23. playlists: playlist_repository.all,
  24. device: repository.find(parameters[:id]),
  25. layout: htmx_layout.call(request)
  26. }
  27. end
  28. end
  29. end
  30. end
  31. end

app/actions/devices/index.rb

100.0% lines covered

100.0% branches covered

18 relevant lines. 18 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Devices
  5. # The index action.
  6. 1 class Index < Action
  7. 1 include Deps[:htmx, repository: "repositories.device"]
  8. 1 def handle request, response
  9. 9 query = request.params[:query].to_s
  10. 9 devices = load query
  11. 9 then: 3 if htmx.request? request.env, :trigger, "search"
  12. 3 add_htmx_headers response, query
  13. 3 response.render view, devices:, query:, layout: false
  14. else: 6 else
  15. 6 response.render view, devices:, query:
  16. end
  17. end
  18. 1 private
  19. 1 def load query
  20. 9 then: 5 else: 4 query.empty? ? repository.all : repository.search(:label, query)
  21. end
  22. 1 def add_htmx_headers response, query
  23. 3 then: 1 else: 2 return if query.empty?
  24. 2 htmx.response! response.headers, push_url: routes.path(:devices, query:)
  25. end
  26. end
  27. end
  28. end
  29. end

app/actions/devices/logs/delete.rb

100.0% lines covered

100.0% branches covered

14 relevant lines. 14 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Devices
  5. 1 module Logs
  6. # The delete action.
  7. 1 class Delete < Action
  8. 1 include Deps[repository: "repositories.device_log"]
  9. 1 params do
  10. 1 required(:device_id).filled :integer
  11. 1 required(:id).filled :integer
  12. end
  13. 1 def handle request, response
  14. 2 parameters = request.params
  15. 2 else: 1 then: 1 halt :unprocessable_content unless parameters.valid?
  16. 1 repository.delete_by_device(*parameters.to_h.values_at(:device_id, :id))
  17. 1 response.body = ""
  18. end
  19. end
  20. end
  21. end
  22. end
  23. end

app/actions/devices/logs/index.rb

100.0% lines covered

100.0% branches covered

30 relevant lines. 30 lines covered and 0 lines missed.
8 total branches, 8 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Devices
  5. 1 module Logs
  6. # The index action.
  7. # :reek:DataClump
  8. 1 class Index < Action
  9. 1 include Deps[
  10. :htmx,
  11. device_repository: "repositories.device",
  12. repository: "repositories.device_log"
  13. ]
  14. 1 params do
  15. 1 required(:device_id).filled :integer
  16. 1 optional(:query).maybe :string
  17. end
  18. 1 def handle request, response
  19. 7 parameters = request.params
  20. 7 else: 6 then: 1 halt :unprocessable_content unless parameters.valid?
  21. 6 device = device_repository.find parameters[:device_id]
  22. 6 then: 2 if htmx.request? request.env, :trigger, "search"
  23. 2 render_search_results parameters, device, response
  24. else: 4 else
  25. 4 render_all parameters, device, response
  26. end
  27. end
  28. 1 private
  29. 1 def render_search_results parameters, device, response
  30. 2 query = parameters[:query].to_s
  31. 2 add_htmx_headers response, device, query
  32. 2 response.render view, device:, logs: load(device.id, query), query:, layout: false
  33. end
  34. 1 def render_all parameters, device, response
  35. 4 query = parameters[:query].to_s
  36. 4 response.render view, device:, logs: load(device.id, query), query:
  37. end
  38. 1 def load device_id, query
  39. 6 then: 4 else: 2 return repository.where(device_id:) if query.empty?
  40. 2 repository.search :message, query, device_id:
  41. end
  42. 1 def add_htmx_headers response, device, query
  43. 2 then: 1 else: 1 return if query.empty?
  44. 1 htmx.response! response.headers,
  45. push_url: routes.path(:device_logs, device_id: device.id, query:)
  46. end
  47. end
  48. end
  49. end
  50. end
  51. end

app/actions/devices/logs/show.rb

100.0% lines covered

100.0% branches covered

14 relevant lines. 14 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Devices
  5. 1 module Logs
  6. # The show action.
  7. 1 class Show < Action
  8. 1 include Deps[
  9. device_repository: "repositories.device",
  10. repository: "repositories.device_log"
  11. ]
  12. 1 params do
  13. 1 required(:device_id).filled :integer
  14. 1 required(:id).filled :integer
  15. end
  16. 1 def handle request, response
  17. 1 parameters = request.params
  18. 1 device = device_repository.find parameters[:device_id]
  19. 1 log = repository.find parameters[:id]
  20. 1 response.render view, device:, log:
  21. end
  22. end
  23. end
  24. end
  25. end
  26. end

app/actions/devices/new.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Devices
  5. # The new action.
  6. 1 class New < Action
  7. 1 include Deps[
  8. :htmx_layout,
  9. "aspects.devices.defaulter",
  10. model_repository: "repositories.model",
  11. playlist_repository: "repositories.playlist"
  12. ]
  13. 1 def handle request, response
  14. 3 response.render view,
  15. models: model_repository.all,
  16. playlists: playlist_repository.all,
  17. fields: defaulter.call,
  18. layout: htmx_layout.call(request)
  19. end
  20. end
  21. end
  22. end
  23. end

app/actions/devices/show.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Devices
  5. # The show action.
  6. 1 class Show < Action
  7. 1 include Deps[:htmx_layout, repository: "repositories.device"]
  8. 2 params { required(:id).filled :integer }
  9. 1 def handle request, response
  10. 4 parameters = request.params
  11. 4 else: 3 then: 1 halt :unprocessable_content unless parameters.valid?
  12. 3 response.render view,
  13. device: repository.find(parameters[:id]),
  14. layout: htmx_layout.call(request)
  15. end
  16. end
  17. end
  18. end
  19. end

app/actions/devices/update.rb

100.0% lines covered

100.0% branches covered

20 relevant lines. 20 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Devices
  5. # The update action.
  6. 1 class Update < Action
  7. 1 include Deps[
  8. repository: "repositories.device",
  9. model_repository: "repositories.model",
  10. playlist_repository: "repositories.playlist",
  11. show_view: "views.devices.show"
  12. ]
  13. 1 contract Contracts::Devices::Update
  14. 1 def handle request, response
  15. 3 parameters = request.params
  16. 3 device = repository.find parameters[:id]
  17. 3 else: 2 then: 1 halt :unprocessable_content unless device
  18. 2 then: 1 if parameters.valid?
  19. 1 save device, parameters, response
  20. else: 1 else
  21. 1 error device, parameters, response
  22. end
  23. end
  24. 1 private
  25. 1 def save device, parameters, response
  26. 1 id = device.id
  27. 1 repository.update id, **parameters[:device]
  28. 1 response.render show_view, device: repository.find(id), layout: false
  29. end
  30. 1 def error device, parameters, response
  31. 1 response.render view,
  32. models: model_repository.all,
  33. playlists: playlist_repository.all,
  34. device:,
  35. fields: parameters[:device],
  36. errors: parameters.errors[:device],
  37. layout: false
  38. end
  39. end
  40. end
  41. end
  42. end

app/actions/extensions/build/create.rb

100.0% lines covered

100.0% branches covered

16 relevant lines. 16 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Extensions
  5. 1 module Build
  6. # The create action.
  7. 1 class Create < Action
  8. 1 include Deps[:htmx, repository: "repositories.extension"]
  9. 1 include Initable[job: Jobs::Batches::Extension]
  10. 2 params { required(:extension_id).filled :integer }
  11. 1 def handle request, response
  12. 3 extension = repository.find request.params[:extension_id]
  13. 3 enqueue extension, response
  14. end
  15. 1 private
  16. 1 def enqueue extension, response
  17. 3 job.perform_async extension.id
  18. 3 response.status = 202
  19. 3 response.render view, extension:, layout: false
  20. end
  21. end
  22. end
  23. end
  24. end
  25. end

app/actions/extensions/clone/create.rb

100.0% lines covered

100.0% branches covered

25 relevant lines. 25 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "refinements/hash"
  3. 1 module Terminus
  4. 1 module Actions
  5. 1 module Extensions
  6. 1 module Clone
  7. # The create action.
  8. 1 class Create < Action
  9. 1 include Deps["aspects.extensions.cloner", repository: "repositories.extension"]
  10. 1 using Refinements::Hash
  11. 1 params do
  12. 1 required(:extension_id).filled :integer
  13. 1 required(:extension).filled Schemas::Extensions::Upsert
  14. end
  15. 1 def handle request, response
  16. 3 parameters = request.params
  17. 3 then: 2 if parameters.valid?
  18. 2 save parameters, response
  19. else: 1 else
  20. 1 error parameters, parameters.errors[:extension], response
  21. end
  22. end
  23. 1 private
  24. 1 def save parameters, response
  25. 2 in: 1 case cloner.call parameters[:extension_id], **parameters[:extension]
  26. 1 in: 1 in Success then response.redirect_to routes.path(:extensions)
  27. 1 in Failure(errors) then error parameters, errors, response
  28. skipped # :nocov:
  29. skipped # :nocov:
  30. end
  31. end
  32. 1 def error parameters, errors, response
  33. 2 fields = parameters[:extension].transform_with!(
  34. 2 start_at: -> value { value.strftime("%Y-%m-%dT%H:%M:%S") }
  35. )
  36. 2 response.render view,
  37. extension: repository.find(parameters[:extension_id]),
  38. fields:,
  39. errors:
  40. end
  41. end
  42. end
  43. end
  44. end
  45. end

app/actions/extensions/clone/new.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Extensions
  5. 1 module Clone
  6. # The new action.
  7. 1 class New < Hanami::Action
  8. 1 include Deps[repository: "repositories.extension"]
  9. 2 params { required(:extension_id).filled :integer }
  10. 1 def handle request, response
  11. 1 extension = repository.find request.params[:extension_id]
  12. 1 fields = {label: "#{extension.label} Clone", name: "#{extension.name}_clone"}
  13. 1 response.render view, extension:, fields:
  14. end
  15. end
  16. end
  17. end
  18. end
  19. end

app/actions/extensions/create.rb

100.0% lines covered

100.0% branches covered

25 relevant lines. 25 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "refinements/hash"
  3. 1 module Terminus
  4. 1 module Actions
  5. 1 module Extensions
  6. # The create action.
  7. 1 class Create < Action
  8. 1 include Deps[
  9. :htmx_layout,
  10. "aspects.jobs.schedule",
  11. repository: "repositories.extension",
  12. index_view: "views.extensions.index"
  13. ]
  14. 1 using Refinements::Hash
  15. 1 contract Contracts::Extensions::Create
  16. 1 def handle request, response
  17. 4 parameters = request.params
  18. 4 then: 3 if parameters.valid?
  19. 3 save parameters
  20. 3 response.render index_view,
  21. extensions: repository.all,
  22. layout: htmx_layout.call(request)
  23. else: 1 else
  24. 1 error response, parameters
  25. end
  26. end
  27. 1 private
  28. 1 def save parameters
  29. 3 attributes = parameters[:extension]
  30. 3 model_ids, device_ids = attributes.values_at :model_ids, :device_ids
  31. 3 extension = repository.create_with_models attributes, Array(model_ids)
  32. 3 repository.update_with_devices extension.id, {}, Array(device_ids)
  33. 3 schedule.upsert(*extension.to_schedule)
  34. end
  35. 1 def error response, parameters
  36. 1 fields = parameters[:extension].transform_with!(
  37. 1 start_at: -> value { value.strftime("%Y-%m-%dT%H:%M:%S") }
  38. )
  39. 1 response.render view, fields:, errors: parameters.errors[:extension], layout: false
  40. end
  41. end
  42. end
  43. end
  44. end

app/actions/extensions/delete.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Extensions
  5. # The delete action.
  6. 1 class Delete < Action
  7. 1 include Deps["aspects.jobs.schedule", repository: "repositories.extension"]
  8. 1 def handle request, response
  9. 3 extension = repository.find request.params[:id]
  10. 3 else: 2 then: 1 halt :unprocessable_content unless extension
  11. 2 repository.delete extension.id
  12. 2 schedule.delete extension.screen_name
  13. 2 response.body = ""
  14. end
  15. end
  16. end
  17. end
  18. end

app/actions/extensions/edit.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Extensions
  5. # The edit action.
  6. 1 class Edit < Action
  7. 1 include Deps[repository: "repositories.extension"]
  8. 2 params { required(:id).filled :integer }
  9. 1 def handle request, response
  10. 6 parameters = request.params
  11. 6 else: 5 then: 1 halt :unprocessable_content unless parameters.valid?
  12. 5 response.render view, extension: repository.find(parameters[:id])
  13. end
  14. end
  15. end
  16. end
  17. end

app/actions/extensions/exchanges/create.rb

100.0% lines covered

100.0% branches covered

22 relevant lines. 22 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "initable"
  3. 1 module Terminus
  4. 1 module Actions
  5. 1 module Extensions
  6. 1 module Exchanges
  7. # The create action.
  8. 1 class Create < Action
  9. 1 include Deps[
  10. :htmx,
  11. extension_repository: "repositories.extension",
  12. repository: "repositories.extension_exchange"
  13. ]
  14. 1 include Initable[job: Jobs::Extensions::ExchangeRefresh]
  15. 1 contract Contracts::Extensions::Exchanges::Create
  16. 1 def handle request, response
  17. 3 parameters = request.params
  18. 3 then: 2 if parameters.valid?
  19. 2 save parameters, response
  20. else: 1 else
  21. 1 error parameters, response
  22. end
  23. end
  24. 1 private
  25. 1 def save parameters, response
  26. 2 extension_id, exchange = parameters.to_h.values_at :extension_id, :exchange
  27. 2 job.perform_async repository.create(extension_id:, **exchange).id
  28. 2 response.redirect_to routes.path(
  29. :extension_exchanges,
  30. extension_id: parameters[:extension_id]
  31. )
  32. end
  33. 1 def error parameters, response
  34. 1 extension_id, fields = parameters.to_h.values_at :extension_id, :exchange
  35. 1 response.render view,
  36. extension: extension_repository.find(extension_id),
  37. fields:,
  38. errors: parameters.errors[:exchange]
  39. end
  40. end
  41. end
  42. end
  43. end
  44. end

app/actions/extensions/exchanges/delete.rb

100.0% lines covered

100.0% branches covered

15 relevant lines. 15 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Extensions
  5. 1 module Exchanges
  6. # The delete action.
  7. 1 class Delete < Action
  8. 1 include Deps[repository: "repositories.extension_exchange"]
  9. 1 params do
  10. 1 required(:extension_id).filled :integer
  11. 1 required(:id).filled :integer
  12. end
  13. 1 def handle request, response
  14. 3 parameters = request.params
  15. 3 else: 2 then: 1 halt :unprocessable_content unless parameters.valid?
  16. 2 record = repository.find_by(**parameters)
  17. 2 repository.delete record.id
  18. 2 response.body = ""
  19. end
  20. end
  21. end
  22. end
  23. end
  24. end

app/actions/extensions/exchanges/edit.rb

100.0% lines covered

100.0% branches covered

16 relevant lines. 16 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Extensions
  5. 1 module Exchanges
  6. # The edit action.
  7. 1 class Edit < Action
  8. 1 include Deps[
  9. :htmx_layout,
  10. extension_repository: "repositories.extension",
  11. repository: "repositories.extension_exchange"
  12. ]
  13. 1 params do
  14. 1 required(:extension_id).filled :integer
  15. 1 required(:id).filled :integer
  16. end
  17. 1 def handle request, response
  18. 4 parameters = request.params
  19. 4 else: 3 then: 1 halt :unprocessable_content unless parameters.valid?
  20. 3 response.render view, **view_settings(request, parameters)
  21. end
  22. 1 private
  23. 1 def view_settings request, parameters
  24. {
  25. 3 extension: extension_repository.find(parameters[:extension_id]),
  26. exchange: repository.find_by(**parameters),
  27. layout: htmx_layout.call(request)
  28. }
  29. end
  30. end
  31. end
  32. end
  33. end
  34. end

app/actions/extensions/exchanges/index.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Extensions
  5. 1 module Exchanges
  6. # The index action.
  7. 1 class Index < Action
  8. 1 include Deps[
  9. :htmx,
  10. extension_repository: "repositories.extension",
  11. repository: "repositories.extension_exchange"
  12. ]
  13. 1 def handle request, response
  14. 4 response.render view, **view_settings(request)
  15. end
  16. 1 def view_settings request
  17. 4 parameters = request.params
  18. 4 extension = extension_repository.find parameters[:extension_id]
  19. 4 {extension:, exchanges: repository.where(extension_id: extension.id)}
  20. end
  21. end
  22. end
  23. end
  24. end
  25. end

app/actions/extensions/exchanges/new.rb

100.0% lines covered

100.0% branches covered

14 relevant lines. 14 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Extensions
  5. 1 module Exchanges
  6. # The new action.
  7. 1 class New < Action
  8. 1 include Deps[:htmx_layout, extension_repository: "repositories.extension"]
  9. 2 params { required(:extension_id).filled :integer }
  10. 1 def handle request, response
  11. 4 parameters = request.params
  12. 4 else: 3 then: 1 halt 422 unless parameters.valid?
  13. 3 response.render view, **view_settings(request, parameters)
  14. end
  15. 1 private
  16. 1 def view_settings request, parameters
  17. {
  18. 3 extension: extension_repository.find(parameters[:extension_id]),
  19. fields: {verb: "get"},
  20. layout: htmx_layout.call(request)
  21. }
  22. end
  23. end
  24. end
  25. end
  26. end
  27. end

app/actions/extensions/exchanges/update.rb

100.0% lines covered

100.0% branches covered

24 relevant lines. 24 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "initable"
  3. 1 module Terminus
  4. 1 module Actions
  5. 1 module Extensions
  6. 1 module Exchanges
  7. # The update action.
  8. 1 class Update < Action
  9. 1 include Deps[
  10. extension_repository: "repositories.extension",
  11. repository: "repositories.extension_exchange"
  12. ]
  13. 1 include Initable[job: Jobs::Extensions::ExchangeRefresh]
  14. 1 contract Contracts::Extensions::Exchanges::Update
  15. 1 def handle request, response
  16. 2 parameters = request.params
  17. 2 extension_id, id = parameters.to_h.values_at :extension_id, :id
  18. 2 exchange = repository.find_by(extension_id:, id:)
  19. 2 then: 1 if parameters.valid?
  20. 1 save exchange, parameters, response
  21. else: 1 else
  22. 1 error exchange, parameters, response
  23. end
  24. end
  25. 1 private
  26. 1 def save exchange, parameters, response
  27. 1 id = exchange.id
  28. 1 repository.update id, **parameters[:exchange]
  29. 1 job.perform_async id
  30. 1 response.redirect_to routes.path(
  31. :extension_exchanges,
  32. extension_id: exchange.extension_id
  33. )
  34. end
  35. 1 def error exchange, parameters, response
  36. 1 response.render view,
  37. extension: extension_repository.find(exchange.extension_id),
  38. exchange:,
  39. fields: parameters[:exchange],
  40. errors: parameters.errors[:exchange]
  41. end
  42. end
  43. end
  44. end
  45. end
  46. end

app/actions/extensions/export/show.rb

100.0% lines covered

100.0% branches covered

18 relevant lines. 18 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "refinements/hash"
  3. 1 module Terminus
  4. 1 module Actions
  5. 1 module Extensions
  6. 1 module Export
  7. # The show action.
  8. 1 class Show < Action
  9. 1 config.formats.accept :yml
  10. 1 include Deps["aspects.extensions.exporter", repository: "repositories.extension"]
  11. 1 using Refinements::Hash
  12. 1 using Refines::Actions::Response
  13. 2 params { required(:extension_id).filled :integer }
  14. 1 def handle request, response
  15. 4 parameters = request.params
  16. 4 else: 3 then: 1 halt :unprocessable_content unless parameters.valid?
  17. 3 extension = repository.find parameters[:extension_id]
  18. 3 in: 2 case exporter.call extension
  19. 2 in: 1 in Success(body) then response.with body: body.deep_stringify_keys!.to_yaml
  20. 1 in Failure(message) then response.with body: {"error" => message}.to_yaml
  21. skipped # :nocov:
  22. skipped # :nocov:
  23. end
  24. end
  25. end
  26. end
  27. end
  28. end
  29. end

app/actions/extensions/gallery/index.rb

100.0% lines covered

100.0% branches covered

30 relevant lines. 30 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Extensions
  5. 1 module Gallery
  6. # The index action.
  7. 1 class Index < Action
  8. 1 include Deps[:htmx, trmnl_api: :trmnl_api_recipes]
  9. 1 include Initable[empty_recipe: proc { TRMNL::API::Models::Recipe.empty }]
  10. 1 params do
  11. 1 optional(:query).filled :string
  12. 1 optional(:page).filled :integer
  13. end
  14. 1 def handle request, response
  15. 9 parameters = request.params
  16. 17 load(parameters).either -> recipe { render request, recipe, response },
  17. 1 -> message { render_error parameters, message, response }
  18. end
  19. 1 private
  20. 1 def load parameters
  21. 9 in: 1 case parameters
  22. 1 in: 5 in query:, page: then trmnl_api.recipes(search: query, page:)
  23. 5 in: 1 in query: then trmnl_api.recipes search: query
  24. 1 else: 2 in page: then trmnl_api.recipes(page:)
  25. 2 else trmnl_api.recipes
  26. end
  27. end
  28. 1 def render request, recipe, response
  29. 8 query, page = request.params.to_h.values_at :query, :page
  30. 8 then: 2 if htmx.request(**request.env).request?
  31. 2 htmx.response! response.headers,
  32. push_url: routes.path(:extensions_gallery, query:, page:)
  33. 2 response.render view, recipe:, query:, page:, layout: false
  34. else: 6 else
  35. 6 response.render view, recipe:, query:, page:
  36. end
  37. end
  38. 1 def render_error parameters, message, response
  39. 1 response.flash.now[:alert] = message
  40. 1 response.render view, recipe: empty_recipe, **parameters.to_h.slice(:query, :page)
  41. end
  42. end
  43. end
  44. end
  45. end
  46. end

app/actions/extensions/index.rb

100.0% lines covered

100.0% branches covered

17 relevant lines. 17 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Extensions
  5. # The index action.
  6. 1 class Index < Action
  7. 1 include Deps[:htmx, repository: "repositories.extension"]
  8. 1 def handle request, response
  9. 10 query = request.params[:query].to_s
  10. 10 extensions = load query
  11. 10 then: 3 if htmx.request? request.env, :trigger, "search"
  12. 3 add_htmx_headers response, query
  13. 3 response.render view, extensions:, query:, layout: false
  14. else: 7 else
  15. 7 response.render view, extensions:, query:
  16. end
  17. end
  18. 1 private
  19. 11 then: 6 else: 4 def load(query) = query.empty? ? repository.all : repository.search(:label, query)
  20. 1 def add_htmx_headers response, query
  21. 3 then: 1 else: 2 return if query.empty?
  22. 2 htmx.response! response.headers, push_url: routes.path(:extensions, query:)
  23. end
  24. end
  25. end
  26. end
  27. end

app/actions/extensions/new.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Extensions
  5. # The new action.
  6. 1 class New < Action
  7. 1 include Deps[:htmx_layout]
  8. 1 def initialize(defaults: Aspects::Extensions::DEFAULTS, **)
  9. 3 @defaults = defaults
  10. 3 super(**)
  11. end
  12. 1 def handle request, response
  13. 3 response.render view, fields: defaults, layout: htmx_layout.call(request)
  14. end
  15. 1 private
  16. 1 attr_reader :defaults
  17. end
  18. end
  19. end
  20. end

app/actions/extensions/preview/show.rb

100.0% lines covered

100.0% branches covered

21 relevant lines. 21 lines covered and 0 lines missed.
5 total branches, 5 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Extensions
  5. 1 module Preview
  6. # The show action.
  7. 1 class Show < Action
  8. 1 include Deps[
  9. "aspects.extensions.renderer",
  10. repository: "repositories.extension",
  11. view: "views.extensions.dynamic"
  12. ]
  13. 1 params do
  14. 1 required(:extension_id).filled :integer
  15. 1 required(:model_id).filled :integer
  16. 1 required(:device_id).maybe :integer
  17. end
  18. 1 def handle request, response
  19. 8 id, model_id, device_id = request.params.to_h.values_at :extension_id,
  20. :model_id,
  21. :device_id
  22. 8 extension = repository.find id
  23. 8 else: 7 then: 1 halt :not_found unless extension
  24. 7 response.render view, content: content_for(extension, model_id, device_id)
  25. end
  26. 1 private
  27. 1 def content_for extension, model_id, device_id
  28. 7 in: 5 case renderer.call(extension, model_id:, device_id:)
  29. 5 in: 1 in Success(content) then content
  30. 1 else: 1 in Failure(message) then message
  31. 1 else "Unable to render body for extension: #{extension.id}."
  32. end
  33. end
  34. end
  35. end
  36. end
  37. end
  38. end

app/actions/extensions/sensors/index.rb

100.0% lines covered

100.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "refinements/hash"
  3. 1 module Terminus
  4. 1 module Actions
  5. 1 module Extensions
  6. 1 module Sensors
  7. # The index action.
  8. 1 class Index < Action
  9. 1 include Deps[
  10. repository: "repositories.extension",
  11. sensor_repository: "repositories.device_sensor"
  12. ]
  13. 1 include Initable[json_formatter: Aspects::JSONFormatter]
  14. 1 using Refinements::Hash
  15. 2 params { required(:extension_id).filled :integer }
  16. 1 def handle request, response
  17. 10 extension = repository.find request.params[:extension_id]
  18. 10 else: 9 then: 1 halt :not_found unless extension
  19. 9 content = load_content extension
  20. 9 response.render view, content: json_formatter.call(content), layout: false
  21. end
  22. 1 def load_content extension
  23. 9 device_ids = extension.devices.map(&:id)
  24. 9 sensors = sensor_repository.limited_where device_id: device_ids
  25. 9 {sensors: sensors.map(&:liquid_attributes)}
  26. end
  27. end
  28. end
  29. end
  30. end
  31. end

app/actions/extensions/sources/index.rb

100.0% lines covered

100.0% branches covered

17 relevant lines. 17 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Extensions
  5. 1 module Sources
  6. # The index action.
  7. 1 class Index < Action
  8. 1 include Deps[:htmx_layout, repository: "repositories.extension_exchange"]
  9. 2 params { required(:extension_id).filled :integer }
  10. 1 def initialize(
  11. coalescer: Aspects::Extensions::Exchanges::Coalescer,
  12. json_formatter: Aspects::JSONFormatter,
  13. **
  14. )
  15. 10 @coalescer = coalescer
  16. 10 @json_formatter = json_formatter
  17. 10 super(**)
  18. end
  19. 1 def handle request, response
  20. 10 exchanges = repository.where extension_id: request.params[:extension_id]
  21. 10 content = json_formatter.call coalescer.call(exchanges)
  22. 10 response.render view, content:, layout: htmx_layout.call(request)
  23. end
  24. 1 private
  25. 1 attr_reader :coalescer, :json_formatter
  26. end
  27. end
  28. end
  29. end
  30. end

app/actions/extensions/update.rb

100.0% lines covered

100.0% branches covered

30 relevant lines. 30 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "refinements/hash"
  3. 1 module Terminus
  4. 1 module Actions
  5. 1 module Extensions
  6. # The update action.
  7. 1 class Update < Action
  8. 1 include Deps["aspects.jobs.schedule", repository: "repositories.extension"]
  9. 1 using Refinements::Hash
  10. 1 contract Contracts::Extensions::Update
  11. 1 def handle request, response
  12. 3 parameters = request.params
  13. 3 extension = repository.find parameters[:id]
  14. 3 else: 2 then: 1 halt :unprocessable_content unless extension
  15. 2 then: 1 if parameters.valid?
  16. 1 render extension, parameters, response
  17. else: 1 else
  18. 1 error extension, parameters, response
  19. end
  20. end
  21. 1 private
  22. 1 def render extension, parameters, response
  23. 1 update extension, parameters[:extension]
  24. 1 response.flash[:notice] = "Changes saved."
  25. 1 response.redirect_to routes.path(:extension_edit, id: extension.id)
  26. end
  27. 1 def update extension, attributes
  28. 1 id = extension.id
  29. 1 model_ids, device_ids = attributes.values_at :model_ids, :device_ids
  30. 1 repository.update_with_devices id, attributes, Array(device_ids)
  31. 1 extension = repository.update_with_models id, attributes, Array(model_ids)
  32. 1 schedule.upsert(*extension.to_schedule, old_name: extension.screen_name)
  33. end
  34. 1 def error extension, parameters, response
  35. 1 fields = parameters[:extension].transform_with!(
  36. 1 start_at: -> value { value.strftime("%Y-%m-%dT%H:%M:%S") }
  37. )
  38. 1 response.render view, extension:, fields:, errors: parameters.errors[:extension]
  39. end
  40. end
  41. end
  42. end
  43. end

app/actions/firmware/create.rb

100.0% lines covered

100.0% branches covered

24 relevant lines. 24 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Firmware
  5. # The create action.
  6. 1 class Create < Action
  7. 1 include Deps[
  8. :htmx,
  9. repository: "repositories.firmware",
  10. index_view: "views.firmware.index"
  11. ]
  12. 1 params do
  13. 1 required(:firmware).filled :hash do
  14. 1 required(:version).filled Types::Version
  15. 1 required(:kind).filled :string
  16. 1 required(:attachment).filled :hash
  17. end
  18. end
  19. 1 def handle request, response
  20. 3 parameters = request.params
  21. 3 then: 1 if parameters.valid?
  22. 1 save parameters[:firmware]
  23. 1 response.render index_view, firmware: repository.all
  24. else: 2 else
  25. 2 error response, parameters
  26. end
  27. end
  28. 1 private
  29. # :reek:FeatureEnvy
  30. 1 def save attributes
  31. 1 attachment = attributes.delete :attachment
  32. 1 record = repository.create attributes
  33. 1 record.upload attachment[:tempfile], metadata: {"filename" => "#{record.version}.bin"}
  34. 1 repository.update record.id, attachment_data: record.attachment_attributes
  35. end
  36. 1 def error response, parameters
  37. 2 response.render view,
  38. firmware: nil,
  39. fields: parameters[:firmware],
  40. errors: parameters.errors[:firmware]
  41. end
  42. end
  43. end
  44. end
  45. end

app/actions/firmware/delete.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Firmware
  5. # The delete action.
  6. 1 class Delete < Action
  7. 1 include Deps[repository: "repositories.firmware"]
  8. 2 params { required(:id).filled :integer }
  9. 1 def handle request, response
  10. 5 parameters = request.params
  11. 5 else: 4 then: 1 halt :unprocessable_content unless parameters.valid?
  12. 4 repository.delete parameters[:id]
  13. 4 response.body = ""
  14. end
  15. end
  16. end
  17. end
  18. end

app/actions/firmware/edit.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Firmware
  5. # The edit action.
  6. 1 class Edit < Action
  7. 1 include Deps[:htmx_layout, repository: "repositories.firmware"]
  8. 2 params { required(:id).filled :integer }
  9. 1 def handle request, response
  10. 5 parameters = request.params
  11. 5 else: 4 then: 1 halt :unprocessable_content unless parameters.valid?
  12. 4 response.render view,
  13. firmware: repository.find(parameters[:id]),
  14. layout: htmx_layout.call(request)
  15. end
  16. end
  17. end
  18. end
  19. end

app/actions/firmware/index.rb

100.0% lines covered

100.0% branches covered

16 relevant lines. 16 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Firmware
  5. # The index action.
  6. 1 class Index < Action
  7. 1 include Deps[:htmx, repository: "repositories.firmware"]
  8. 1 def handle request, response
  9. 11 query = request.params[:query]
  10. 11 firmware = load query
  11. 11 then: 3 if htmx.request? request.env, :trigger, "search"
  12. 3 add_htmx_headers response, query
  13. 3 response.render view, firmware:, query:, layout: false
  14. else: 8 else
  15. 8 response.render view, firmware:, query:
  16. end
  17. end
  18. 1 private
  19. 12 then: 5 else: 6 def load(query) = query ? repository.search(:version, query) : repository.all
  20. 1 def add_htmx_headers response, query
  21. 3 htmx.response! response.headers, push_url: routes.path(:firmware, query:)
  22. end
  23. end
  24. end
  25. end
  26. end

app/actions/firmware/new.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Firmware
  5. # The new action.
  6. 1 class New < Action
  7. 1 include Deps[:htmx_layout]
  8. 1 def handle request, response
  9. 3 response.render view, fields: {kind: "terminus"}, layout: htmx_layout.call(request)
  10. end
  11. end
  12. end
  13. end
  14. end

app/actions/firmware/show.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Firmware
  5. # The show action.
  6. 1 class Show < Action
  7. 1 include Deps[:htmx_layout, repository: "repositories.firmware"]
  8. 2 params { required(:id).filled :integer }
  9. 1 def handle request, response
  10. 5 parameters = request.params
  11. 5 else: 4 then: 1 halt :unprocessable_content unless parameters.valid?
  12. 4 response.render view,
  13. firmware: repository.find(parameters[:id]),
  14. layout: htmx_layout.call(request)
  15. end
  16. end
  17. end
  18. end
  19. end

app/actions/firmware/update.rb

100.0% lines covered

100.0% branches covered

32 relevant lines. 32 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Firmware
  5. # The update action.
  6. 1 class Update < Action
  7. 1 include Deps[repository: "repositories.firmware"]
  8. 1 params do
  9. 1 required(:id).filled :integer
  10. 1 required(:firmware).filled :hash do
  11. 1 required(:version).filled Types::Version
  12. 1 required(:kind).filled :string
  13. 1 optional(:attachment).filled :hash
  14. end
  15. end
  16. 1 def handle request, response
  17. 4 parameters = request.params
  18. 4 record = repository.find parameters[:id]
  19. 4 else: 3 then: 1 halt :unprocessable_content unless record
  20. 3 then: 2 if parameters.valid?
  21. 2 save record, parameters, response
  22. else: 1 else
  23. 1 error record, parameters, response
  24. end
  25. end
  26. 1 private
  27. # :reek:TooManyStatements
  28. 1 def save record, parameters, response
  29. 2 id = record.id
  30. 2 attributes = parameters[:firmware]
  31. 2 attachment = attributes.delete :attachment
  32. 2 repository.update id, **attributes
  33. 2 attach record, attachment
  34. 2 response.redirect_to routes.path(:firmware_show, id:)
  35. end
  36. # :reek:FeatureEnvy
  37. 1 def attach record, attachment
  38. 2 else: 1 then: 1 return unless attachment
  39. 1 record.replace attachment[:tempfile], metadata: {"filename" => "#{record.version}.bin"}
  40. 1 repository.update record.id, attachment_data: record.attachment_attributes
  41. end
  42. 1 def error record, parameters, response
  43. 1 response.render view,
  44. firmware: record,
  45. fields: parameters[:firmware],
  46. errors: parameters.errors[:firmware]
  47. end
  48. end
  49. end
  50. end
  51. end

app/actions/models/clone/create.rb

100.0% lines covered

100.0% branches covered

20 relevant lines. 20 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Models
  5. 1 module Clone
  6. # The create action.
  7. 1 class Create < Action
  8. 1 include Deps["aspects.models.cloner", repository: "repositories.model"]
  9. 1 contract Contracts::Models::Clone
  10. 1 def handle request, response
  11. 3 parameters = request.params
  12. 3 then: 2 if parameters.valid?
  13. 2 clone parameters, response
  14. else: 1 else
  15. 1 render_form_error parameters, parameters.errors[:model], response
  16. end
  17. end
  18. 1 private
  19. 1 def clone parameters, response
  20. 2 in: 1 case cloner.call parameters[:model_id], **parameters[:model]
  21. 1 in: 1 in Success then response.redirect_to routes.path(:models)
  22. 1 in Failure(errors) then render_form_error parameters, errors, response
  23. skipped # :nocov:
  24. skipped # :nocov:
  25. end
  26. end
  27. 1 def render_form_error parameters, errors, response
  28. 2 id, fields = parameters.to_h.values_at :model_id, :model
  29. 2 response.render view, model: repository.find(id), fields:, errors:
  30. end
  31. end
  32. end
  33. end
  34. end
  35. end

app/actions/models/clone/new.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Models
  5. 1 module Clone
  6. # The new action.
  7. 1 class New < Action
  8. 1 include Deps[repository: "repositories.model"]
  9. 1 def handle request, response
  10. 1 model = repository.find request.params[:model_id]
  11. 1 fields = {label: "#{model.label} Clone", name: "#{model.name}_clone"}
  12. 1 response.render view, model:, fields:
  13. end
  14. end
  15. end
  16. end
  17. end
  18. end

app/actions/models/create.rb

100.0% lines covered

100.0% branches covered

16 relevant lines. 16 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Models
  5. # The create action.
  6. 1 class Create < Action
  7. 1 include Deps[
  8. :htmx_layout,
  9. repository: "repositories.model",
  10. index_view: "views.models.index"
  11. ]
  12. 1 contract Contracts::Models::Create
  13. 1 def handle request, response
  14. 4 parameters = request.params
  15. 4 then: 3 if parameters.valid?
  16. 3 repository.create parameters[:model]
  17. 3 response.render index_view, **view_settings(request)
  18. else: 1 else
  19. 1 error response, parameters
  20. end
  21. end
  22. 1 private
  23. 1 def view_settings(request) = {models: repository.all, layout: htmx_layout.call(request)}
  24. 1 def error response, parameters
  25. 1 response.render view,
  26. models: repository.all,
  27. fields: parameters[:model],
  28. errors: parameters.errors[:model],
  29. layout: false
  30. end
  31. end
  32. end
  33. end
  34. end

app/actions/models/delete.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Models
  5. # The delete action.
  6. 1 class Delete < Action
  7. 1 include Deps[repository: "repositories.model"]
  8. 2 params { required(:id).filled :integer }
  9. 1 def handle request, response
  10. 3 parameters = request.params
  11. 3 else: 2 then: 1 halt :unprocessable_content unless parameters.valid?
  12. 2 repository.delete parameters[:id]
  13. 2 response.body = ""
  14. end
  15. end
  16. end
  17. end
  18. end

app/actions/models/edit.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Models
  5. # The edit action.
  6. 1 class Edit < Action
  7. 1 include Deps[:htmx_layout, repository: "repositories.model"]
  8. 2 params { required(:id).filled :integer }
  9. 1 def handle request, response
  10. 4 parameters = request.params
  11. 4 else: 3 then: 1 halt :unprocessable_content unless parameters.valid?
  12. 3 response.render view,
  13. model: repository.find(parameters[:id]),
  14. layout: htmx_layout.call(request)
  15. end
  16. end
  17. end
  18. end
  19. end

app/actions/models/index.rb

100.0% lines covered

100.0% branches covered

17 relevant lines. 17 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Models
  5. # The index action.
  6. 1 class Index < Action
  7. 1 include Deps[:htmx, repository: "repositories.model"]
  8. 1 def handle request, response
  9. 10 query = request.params[:query].to_s
  10. 10 models = load query
  11. 10 then: 3 if htmx.request? request.env, :trigger, "search"
  12. 3 add_htmx_headers response, query
  13. 3 response.render view, models:, query:, layout: false
  14. else: 7 else
  15. 7 response.render view, models:, query:
  16. end
  17. end
  18. 1 private
  19. 11 then: 6 else: 4 def load(query) = query.empty? ? repository.all : repository.search(:label, query)
  20. 1 def add_htmx_headers response, query
  21. 3 then: 1 else: 2 return if query.empty?
  22. 2 htmx.response! response.headers, push_url: routes.path(:models, query:)
  23. end
  24. end
  25. end
  26. end
  27. end

app/actions/models/new.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Models
  5. # The new action.
  6. 1 class New < Action
  7. 1 include Deps[:htmx_layout]
  8. 1 def initialize(defaults: Aspects::Models::DEFAULTS, **)
  9. 3 @defaults = defaults
  10. 3 super(**)
  11. end
  12. 1 def handle request, response
  13. 3 response.render view, fields: defaults, layout: htmx_layout.call(request)
  14. end
  15. 1 private
  16. 1 attr_reader :defaults
  17. end
  18. end
  19. end
  20. end

app/actions/models/show.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Models
  5. # The show action.
  6. 1 class Show < Action
  7. 1 include Deps[:htmx_layout, repository: "repositories.model"]
  8. 2 params { required(:id).filled :integer }
  9. 1 def handle request, response
  10. 3 parameters = request.params
  11. 3 else: 2 then: 1 halt :unprocessable_content unless parameters.valid?
  12. 2 response.render view,
  13. model: repository.find(parameters[:id]),
  14. layout: htmx_layout.call(request)
  15. end
  16. end
  17. end
  18. end
  19. end

app/actions/models/update.rb

100.0% lines covered

100.0% branches covered

20 relevant lines. 20 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Models
  5. # The update action.
  6. 1 class Update < Action
  7. 1 include Deps[repository: "repositories.model", show_view: "views.models.show"]
  8. 1 contract Contracts::Models::Update
  9. 1 def handle request, response
  10. 3 parameters = request.params
  11. 3 model = repository.find parameters[:id]
  12. 3 else: 2 then: 1 halt :unprocessable_content unless model
  13. 2 then: 1 if parameters.valid?
  14. 1 save model, parameters, response
  15. else: 1 else
  16. 1 error model, parameters, response
  17. end
  18. end
  19. 1 private
  20. 1 def save model, parameters, response
  21. 1 id = model.id
  22. 1 repository.update id, **parameters[:model]
  23. 1 response.render show_view, model: repository.find(id), layout: false
  24. end
  25. 1 def error model, parameters, response
  26. 1 response.render view,
  27. model:,
  28. fields: parameters[:model],
  29. errors: parameters.errors[:model],
  30. layout: false
  31. end
  32. end
  33. end
  34. end
  35. end

app/actions/playlists/clone/create.rb

100.0% lines covered

100.0% branches covered

25 relevant lines. 25 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Playlists
  5. 1 module Clone
  6. # The create action.
  7. 1 class Create < Action
  8. 1 include Deps["aspects.playlists.cloner", repository: "repositories.playlist"]
  9. 1 params do
  10. 1 required(:playlist_id).filled :integer
  11. 1 required(:playlist).hash do
  12. 1 required(:label).filled :string
  13. 1 required(:name).filled :string
  14. 1 required(:mode).filled :string
  15. end
  16. end
  17. 1 def handle request, response
  18. 3 parameters = request.params
  19. 3 then: 2 if parameters.valid?
  20. 2 clone parameters, response
  21. else: 1 else
  22. 1 render_form_error parameters, parameters.errors[:playlist], response
  23. end
  24. end
  25. 1 private
  26. 1 def clone parameters, response
  27. 2 in: 1 case cloner.call parameters[:playlist_id], **parameters[:playlist]
  28. 1 in: 1 in Success then response.redirect_to routes.path(:playlists)
  29. 1 in Failure(errors) then render_form_error parameters, errors, response
  30. skipped # :nocov:
  31. skipped # :nocov:
  32. end
  33. end
  34. 1 def render_form_error parameters, errors, response
  35. 2 id, fields = parameters.to_h.values_at :playlist_id, :playlist
  36. 2 response.render view, playlist: repository.find(id), fields:, errors:
  37. end
  38. end
  39. end
  40. end
  41. end
  42. end

app/actions/playlists/clone/new.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Playlists
  5. 1 module Clone
  6. # The new action.
  7. 1 class New < Action
  8. 1 include Deps[repository: "repositories.playlist"]
  9. 1 def handle request, response
  10. 1 playlist = repository.find request.params[:playlist_id]
  11. 1 fields = {label: "#{playlist.label} Clone", name: "#{playlist.name}_clone"}
  12. 1 response.render view, playlist:, fields:
  13. end
  14. end
  15. end
  16. end
  17. end
  18. end

app/actions/playlists/create.rb

100.0% lines covered

100.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Playlists
  5. # The create action.
  6. 1 class Create < Action
  7. 1 include Deps[
  8. :htmx_layout,
  9. repository: "repositories.playlist",
  10. index_view: "views.playlists.index"
  11. ]
  12. 1 params do
  13. 1 required(:playlist).hash do
  14. 1 required(:label).filled :string
  15. 1 required(:name).filled :string
  16. 1 required(:mode).filled :string
  17. end
  18. end
  19. 1 def handle request, response
  20. 4 parameters = request.params
  21. 4 then: 3 if parameters.valid?
  22. 3 repository.create parameters[:playlist]
  23. 3 response.render index_view, playlists: repository.all, layout: htmx_layout.call(request)
  24. else: 1 else
  25. 1 error response, parameters
  26. end
  27. end
  28. 1 private
  29. 1 def error response, parameters
  30. 1 response.render view,
  31. playlist: nil,
  32. fields: parameters[:playlist],
  33. errors: parameters.errors[:playlist],
  34. layout: false
  35. end
  36. end
  37. end
  38. end
  39. end

app/actions/playlists/delete.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Playlists
  5. # The delete action.
  6. 1 class Delete < Action
  7. 1 include Deps[repository: "repositories.playlist"]
  8. 2 params { required(:id).filled :integer }
  9. 1 def handle request, response
  10. 3 parameters = request.params
  11. 3 else: 2 then: 1 halt :unprocessable_content unless parameters.valid?
  12. 2 repository.delete parameters[:id]
  13. 2 response.body = ""
  14. end
  15. end
  16. end
  17. end
  18. end

app/actions/playlists/edit.rb

100.0% lines covered

100.0% branches covered

14 relevant lines. 14 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Playlists
  5. # The edit action.
  6. 1 class Edit < Action
  7. 1 include Deps[
  8. :htmx_layout,
  9. repository: "repositories.playlist",
  10. item_repository: "repositories.playlist_item"
  11. ]
  12. 2 params { required(:id).filled :integer }
  13. 1 def handle request, response
  14. 4 parameters = request.params
  15. 4 else: 3 then: 1 halt :unprocessable_content unless parameters.valid?
  16. 3 response.render view, **view_settings(request, parameters)
  17. end
  18. 1 private
  19. 1 def view_settings request, parameters
  20. 3 playlist = repository.find parameters[:id]
  21. {
  22. 3 playlist:,
  23. items: item_repository.where(playlist_id: playlist.id),
  24. layout: htmx_layout.call(request)
  25. }
  26. end
  27. end
  28. end
  29. end
  30. end

app/actions/playlists/index.rb

100.0% lines covered

100.0% branches covered

17 relevant lines. 17 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Playlists
  5. # The index action.
  6. 1 class Index < Action
  7. 1 include Deps[:htmx, repository: "repositories.playlist"]
  8. 1 def handle request, response
  9. 12 query = request.params[:query].to_s
  10. 12 playlists = load query
  11. 12 then: 3 if htmx.request? request.env, :trigger, "search"
  12. 3 add_htmx_headers response, query
  13. 3 response.render view, playlists:, query:, layout: false
  14. else: 9 else
  15. 9 response.render view, playlists:, query:
  16. end
  17. end
  18. 1 private
  19. 13 then: 8 else: 4 def load(query) = query.empty? ? repository.all : repository.search(:label, query)
  20. 1 def add_htmx_headers response, query
  21. 3 then: 1 else: 2 return if query.empty?
  22. 2 htmx.response! response.headers, push_url: routes.path(:playlists, query:)
  23. end
  24. end
  25. end
  26. end
  27. end

app/actions/playlists/items/create.rb

100.0% lines covered

100.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Playlists
  5. 1 module Items
  6. # The create action.
  7. 1 class Create < Action
  8. 1 include Deps[
  9. repository: "repositories.playlist_item",
  10. playlist_repository: "repositories.playlist",
  11. show_view: "views.playlists.items.show"
  12. ]
  13. 1 params do
  14. 1 required(:playlist_id).filled :integer
  15. 2 required(:playlist_item).hash { required(:screen_id).filled :integer }
  16. end
  17. 1 def handle request, response
  18. 3 parameters = request.params
  19. 3 playlist = playlist_repository.find parameters[:playlist_id]
  20. 3 else: 1 then: 2 halt :unprocessable_content unless parameters.valid? && playlist
  21. 1 response.render show_view, item: create(playlist, parameters), layout: false
  22. end
  23. 1 private
  24. 1 def create playlist, parameters
  25. 1 item = repository.create_with_position playlist_id: playlist.id,
  26. **parameters[:playlist_item]
  27. 1 playlist_repository.update_current_item playlist, item
  28. 1 item
  29. end
  30. end
  31. end
  32. end
  33. end
  34. end

app/actions/playlists/items/index.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Playlists
  5. 1 module Items
  6. # The index action.
  7. 1 class Index < Action
  8. 1 include Deps[repository: "repositories.playlist_item"]
  9. 1 def handle request, response
  10. 2 parameters = request.params
  11. 2 playlist_id = parameters[:playlist_id]
  12. 2 response.render view, playlist_id:, items: repository.where(playlist_id:), layout: false
  13. end
  14. end
  15. end
  16. end
  17. end
  18. end

app/actions/playlists/mirror/edit.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Playlists
  5. 1 module Mirror
  6. # The edit action.
  7. 1 class Edit < Action
  8. 1 include Deps[
  9. :htmx_layout,
  10. repository: "repositories.playlist",
  11. device_repository: "repositories.device"
  12. ]
  13. 2 params { required(:playlist_id).filled :integer }
  14. 1 def handle request, response
  15. 5 parameters = request.params
  16. 5 else: 4 then: 1 halt :unprocessable_content unless parameters.valid?
  17. 4 response.render view,
  18. playlist: repository.find(parameters[:playlist_id]),
  19. devices: device_repository.all,
  20. layout: htmx_layout.call(request)
  21. end
  22. end
  23. end
  24. end
  25. end
  26. end

app/actions/playlists/mirror/update.rb

100.0% lines covered

100.0% branches covered

22 relevant lines. 22 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Playlists
  5. 1 module Mirror
  6. # The update action.
  7. 1 class Update < Action
  8. 1 include Deps[
  9. :htmx,
  10. :htmx_layout,
  11. repository: "repositories.playlist",
  12. device_repository: "repositories.device",
  13. playlist_item_repository: "repositories.playlist_item",
  14. view: "views.playlists.show"
  15. ]
  16. 1 params do
  17. 1 required(:playlist_id).filled :integer
  18. 2 optional(:mirror).filled(:hash) { required(:device_ids).array :integer }
  19. end
  20. 1 def handle request, response
  21. 5 parameters = request.params
  22. 5 playlist = repository.find parameters[:playlist_id]
  23. 5 else: 4 then: 1 halt :not_found unless playlist
  24. 4 mirror playlist, parameters
  25. 4 render playlist, request, response
  26. end
  27. 1 private
  28. 1 def mirror playlist, parameters
  29. 4 device_repository.mirror_playlist parameters.dig(:mirror, :device_ids), playlist.id
  30. end
  31. 1 def render playlist, request, response
  32. 4 id = playlist.id
  33. 4 htmx.response! response.headers, push_url: routes.path(:playlist, id:)
  34. 4 response.render view,
  35. playlist:,
  36. items: playlist_item_repository.where(playlist_id: id),
  37. layout: htmx_layout.call(request)
  38. end
  39. end
  40. end
  41. end
  42. end
  43. end

app/actions/playlists/new.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Playlists
  5. # The new action.
  6. 1 class New < Action
  7. 1 include Deps[:htmx_layout]
  8. 1 def handle request, response
  9. 3 response.render view, fields: {mode: :automatic}, layout: htmx_layout.call(request)
  10. end
  11. end
  12. end
  13. end
  14. end

app/actions/playlists/screens/index.rb

100.0% lines covered

100.0% branches covered

17 relevant lines. 17 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Playlists
  5. 1 module Screens
  6. # The index action.
  7. 1 class Index < Action
  8. 1 include Deps[repository: "repositories.playlist", view: "views.playlists.screens.show"]
  9. 1 include Initable[slide_window: Aspects::Playlists::SlideWindow]
  10. 2 params { required(:playlist_id).filled :integer }
  11. 1 def handle request, response
  12. 3 parameters = request.params
  13. 3 else: 2 then: 1 halt :unprocessable_content unless parameters.valid?
  14. 2 window = load_window parameters
  15. 2 before, current, after = window.screens
  16. 2 response.render view, playlist: window.playlist, before:, current:, after:
  17. end
  18. 1 private
  19. 1 def load_window parameters
  20. 2 slide_window.new repository.with_screens.by_pk(parameters[:playlist_id]).one
  21. end
  22. end
  23. end
  24. end
  25. end
  26. end

app/actions/playlists/screens/show.rb

100.0% lines covered

100.0% branches covered

24 relevant lines. 24 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Playlists
  5. 1 module Screens
  6. # The show action.
  7. 1 class Show < Action
  8. 1 include Deps[
  9. :htmx_layout,
  10. repository: "repositories.playlist",
  11. item_repository: "repositories.playlist_item"
  12. ]
  13. 1 include Initable[slide_window: Aspects::Playlists::SlideWindow]
  14. 1 params do
  15. 1 required(:playlist_id).filled :integer
  16. 1 required(:id).filled :integer
  17. end
  18. 1 def handle request, response
  19. 10 parameters = request.params
  20. 10 else: 9 then: 1 halt :unprocessable_content unless parameters.valid?
  21. 9 response.render view, **view_settings(request, update_current_item(parameters))
  22. end
  23. 1 private
  24. 1 def update_current_item parameters
  25. 9 playlist_id = parameters[:playlist_id]
  26. 9 repository.with_screens.by_pk(playlist_id).one.tap do |playlist|
  27. 9 then: 6 else: 3 return playlist if playlist.automatic?
  28. 3 item = item_repository.find_by playlist_id:, screen_id: parameters[:id]
  29. 3 repository.update playlist_id, current_item_id: item.id
  30. end
  31. end
  32. 1 def view_settings request, playlist
  33. 9 before, current, after = slide_window.new(playlist).screens request.params[:id]
  34. {
  35. 9 playlist:,
  36. before:,
  37. current:,
  38. after:,
  39. layout: htmx_layout.call(request)
  40. }
  41. end
  42. end
  43. end
  44. end
  45. end
  46. end

app/actions/playlists/show.rb

100.0% lines covered

100.0% branches covered

14 relevant lines. 14 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Playlists
  5. # The show action.
  6. 1 class Show < Action
  7. 1 include Deps[
  8. :htmx_layout,
  9. repository: "repositories.playlist",
  10. item_repository: "repositories.playlist_item"
  11. ]
  12. 2 params { required(:id).filled :integer }
  13. 1 def handle request, response
  14. 3 parameters = request.params
  15. 3 else: 2 then: 1 halt :unprocessable_content unless parameters.valid?
  16. 2 response.render view, **view_settings(request, parameters)
  17. end
  18. 1 private
  19. 1 def view_settings request, parameters
  20. 2 playlist = repository.find parameters[:id]
  21. {
  22. 2 playlist:,
  23. items: item_repository.where(playlist_id: playlist.id),
  24. layout: htmx_layout.call(request)
  25. }
  26. end
  27. end
  28. end
  29. end
  30. end

app/actions/playlists/update.rb

100.0% lines covered

100.0% branches covered

25 relevant lines. 25 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Playlists
  5. # The update action.
  6. 1 class Update < Action
  7. 1 include Deps[
  8. repository: "repositories.playlist",
  9. item_repository: "repositories.playlist_item",
  10. show_view: "views.playlists.show"
  11. ]
  12. 1 params do
  13. 1 required(:id).filled :integer
  14. 1 required(:playlist).hash do
  15. 1 required(:label).filled :string
  16. 1 required(:name).filled :string
  17. 1 required(:mode).filled :string
  18. end
  19. end
  20. 1 def handle request, response
  21. 3 parameters = request.params
  22. 3 playlist = repository.find parameters[:id]
  23. 3 else: 2 then: 1 halt :unprocessable_content unless playlist
  24. 2 then: 1 if parameters.valid?
  25. 1 save playlist, parameters, response
  26. else: 1 else
  27. 1 error playlist, parameters, response
  28. end
  29. end
  30. 1 private
  31. 1 def save playlist, parameters, response
  32. 1 id = playlist.id
  33. 1 repository.update id, **parameters[:playlist]
  34. 1 response.render show_view,
  35. playlist: repository.find(id),
  36. items: item_repository.where(playlist_id: id),
  37. layout: false
  38. end
  39. 1 def error playlist, parameters, response
  40. 1 response.render view,
  41. playlist:,
  42. fields: parameters[:playlist],
  43. errors: parameters.errors[:playlist],
  44. layout: false
  45. end
  46. end
  47. end
  48. end
  49. end

app/actions/problem_details/index.rb

100.0% lines covered

100.0% branches covered

5 relevant lines. 5 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module ProblemDetails
  5. # The index action.
  6. 1 class Index < Action
  7. 1 def handle(_request, response) = response.render view
  8. end
  9. end
  10. end
  11. end

app/actions/screens/create.rb

100.0% lines covered

100.0% branches covered

27 relevant lines. 27 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Screens
  5. # The create action.
  6. 1 class Create < Action
  7. 1 include Deps[
  8. :htmx,
  9. repository: "repositories.screen",
  10. model_repository: "repositories.model",
  11. index_view: "views.screens.index"
  12. ]
  13. 1 params do
  14. 1 required(:screen).filled(:hash) do
  15. 1 required(:model_id).filled :integer
  16. 1 required(:label).filled :string
  17. 1 required(:name).filled :string
  18. 1 required(:image).filled :hash
  19. end
  20. end
  21. 1 def handle request, response
  22. 2 parameters = request.params
  23. 2 then: 1 if parameters.valid?
  24. 1 save parameters[:screen]
  25. 1 response.render index_view, screens: repository.all
  26. else: 1 else
  27. 1 error response, parameters
  28. end
  29. end
  30. 1 private
  31. # :reek:FeatureEnvy
  32. # :reek:TooManyStatements
  33. 1 def save attributes
  34. 1 image = attributes.delete :image
  35. 1 record = repository.create attributes
  36. 1 tempfile = image[:tempfile]
  37. 1 extension = File.extname tempfile
  38. 1 record.upload tempfile, metadata: {"filename" => "#{record.name}#{extension}"}
  39. 1 repository.update record.id, image_data: record.image_attributes
  40. end
  41. 1 def error response, parameters
  42. 1 response.render view,
  43. models: model_repository.all,
  44. screen: nil,
  45. fields: parameters[:screen],
  46. errors: parameters.errors[:screen]
  47. end
  48. end
  49. end
  50. end
  51. end

app/actions/screens/delete.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Screens
  5. # The delete action.
  6. 1 class Delete < Action
  7. 1 include Deps[repository: "repositories.screen"]
  8. 2 params { required(:id).filled :integer }
  9. 1 def handle request, response
  10. 3 parameters = request.params
  11. 3 else: 2 then: 1 halt :unprocessable_content unless parameters.valid?
  12. 2 repository.delete parameters[:id]
  13. 2 response.body = ""
  14. end
  15. end
  16. end
  17. end
  18. end

app/actions/screens/edit.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Screens
  5. # The edit action.
  6. 1 class Edit < Action
  7. 1 include Deps[
  8. :htmx_layout,
  9. repository: "repositories.screen",
  10. model_repository: "repositories.model"
  11. ]
  12. 2 params { required(:id).filled :integer }
  13. 1 def handle request, response
  14. 5 parameters = request.params
  15. 5 else: 4 then: 1 halt :unprocessable_content unless parameters.valid?
  16. 4 response.render view,
  17. models: model_repository.all,
  18. screen: repository.find(parameters[:id]),
  19. layout: htmx_layout.call(request)
  20. end
  21. end
  22. end
  23. end
  24. end

app/actions/screens/index.rb

100.0% lines covered

100.0% branches covered

17 relevant lines. 17 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Screens
  5. # The index action.
  6. 1 class Index < Action
  7. 1 include Deps[:htmx, repository: "repositories.screen"]
  8. 1 def handle request, response
  9. 8 query = request.params[:query].to_s
  10. 8 screens = load query
  11. 8 then: 3 if htmx.request? request.env, :trigger, "search"
  12. 3 add_htmx_headers response, query
  13. 3 response.render view, screens:, query:, layout: false
  14. else: 5 else
  15. 5 response.render view, screens:, query:
  16. end
  17. end
  18. 1 private
  19. 9 then: 4 else: 4 def load(query) = query.empty? ? repository.all : repository.search(:label, query)
  20. 1 def add_htmx_headers response, query
  21. 3 then: 1 else: 2 return if query.empty?
  22. 2 htmx.response! response.headers, push_url: routes.path(:screens, query:)
  23. end
  24. end
  25. end
  26. end
  27. end

app/actions/screens/new.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Screens
  5. # The new action.
  6. 1 class New < Action
  7. 1 include Deps[:htmx_layout, model_repository: "repositories.model"]
  8. 1 def handle request, response
  9. 3 response.render view, models: model_repository.all, layout: htmx_layout.call(request)
  10. end
  11. end
  12. end
  13. end
  14. end

app/actions/screens/show.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Screens
  5. # The show action.
  6. 1 class Show < Action
  7. 1 include Deps[:htmx_layout, repository: "repositories.screen"]
  8. 2 params { required(:id).filled :integer }
  9. 1 def handle request, response
  10. 5 parameters = request.params
  11. 5 else: 4 then: 1 halt :unprocessable_content unless parameters.valid?
  12. 4 response.render view,
  13. screen: repository.find(parameters[:id]),
  14. layout: htmx_layout.call(request)
  15. end
  16. end
  17. end
  18. end
  19. end

app/actions/screens/update.rb

100.0% lines covered

100.0% branches covered

35 relevant lines. 35 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Screens
  5. # The update action.
  6. 1 class Update < Action
  7. 1 include Deps[repository: "repositories.screen", model_repository: "repositories.model"]
  8. 1 params do
  9. 1 required(:id).filled :integer
  10. 1 required(:screen).filled(:hash) do
  11. 1 required(:model_id).filled :integer
  12. 1 required(:label).filled :string
  13. 1 required(:name).filled :string
  14. 1 optional(:image).filled :hash
  15. end
  16. end
  17. 1 def handle request, response
  18. 4 parameters = request.params
  19. 4 screen = repository.find parameters[:id]
  20. 4 else: 3 then: 1 halt :unprocessable_content unless screen
  21. 3 then: 2 if parameters.valid?
  22. 2 save screen, parameters, response
  23. else: 1 else
  24. 1 error screen, parameters, response
  25. end
  26. end
  27. 1 private
  28. # :reek:TooManyStatements
  29. 1 def save record, parameters, response
  30. 2 id = record.id
  31. 2 attributes = parameters[:screen]
  32. 2 image = attributes.delete :image
  33. 2 repository.update id, **attributes
  34. 2 attach record, image
  35. 2 response.redirect_to routes.path(:screen, id:)
  36. end
  37. # :reek:FeatureEnvy
  38. 1 def attach record, image
  39. 2 else: 1 then: 1 return unless image
  40. 1 tempfile = image[:tempfile]
  41. 1 extension = File.extname tempfile
  42. 1 record.replace tempfile, metadata: {"filename" => "#{record.name}#{extension}"}
  43. 1 repository.update record.id, image_data: record.image_attributes
  44. end
  45. 1 def error screen, parameters, response
  46. 1 response.render view,
  47. models: model_repository.all,
  48. screen:,
  49. fields: parameters[:screen],
  50. errors: parameters.errors[:screen]
  51. end
  52. end
  53. end
  54. end
  55. end

app/actions/users/create.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Users
  5. # The create action.
  6. 1 class Create < Action
  7. 1 include Deps[
  8. :htmx_layout,
  9. repository: "repositories.user",
  10. status_repository: "repositories.user_status",
  11. creator: "aspects.users.creator",
  12. index_view: "views.users.index"
  13. ]
  14. 1 def handle request, response
  15. 4 case creator.call(**request.params.to_h.slice(:user))
  16. in: 3 in Success(Structs::User)
  17. 3 in: 1 response.render index_view, users: repository.all, layout: htmx_layout.call(request)
  18. 1 in Failure(result) then error request, response, result
  19. skipped # :nocov:
  20. skipped # :nocov:
  21. end
  22. end
  23. 1 private
  24. 1 def error request, response, result
  25. 1 response.render view,
  26. user: repository.find(request.params[:id]),
  27. statuses: status_repository.all,
  28. fields: result[:user],
  29. errors: result.errors[:user],
  30. layout: false
  31. end
  32. end
  33. end
  34. end
  35. end

app/actions/users/edit.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Users
  5. # The edit action.
  6. 1 class Edit < Action
  7. 1 include Deps[
  8. :htmx_layout,
  9. repository: "repositories.user",
  10. status_repository: "repositories.user_status"
  11. ]
  12. 2 params { required(:id).filled :integer }
  13. 1 def handle request, response
  14. 4 parameters = request.params
  15. 4 else: 3 then: 1 halt :unprocessable_content unless parameters.valid?
  16. 3 response.render view,
  17. user: repository.find(parameters[:id]),
  18. statuses: status_repository.all,
  19. layout: htmx_layout.call(request)
  20. end
  21. end
  22. end
  23. end
  24. end

app/actions/users/index.rb

100.0% lines covered

100.0% branches covered

17 relevant lines. 17 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Users
  5. # The index action.
  6. 1 class Index < Action
  7. 1 include Deps[:htmx, repository: "repositories.user"]
  8. 1 def handle request, response
  9. 7 query = request.params[:query].to_s
  10. 7 users = load query
  11. 7 then: 3 if htmx.request? request.env, :trigger, "search"
  12. 3 add_htmx_headers response, query
  13. 3 response.render view, users:, query:, layout: false
  14. else: 4 else
  15. 4 response.render view, users:, query:
  16. end
  17. end
  18. 1 private
  19. 8 then: 3 else: 4 def load(query) = query.empty? ? repository.all : repository.search(:name, query)
  20. 1 def add_htmx_headers response, query
  21. 3 then: 1 else: 2 return if query.empty?
  22. 2 htmx.response! response.headers, push_url: routes.path(:users, query:)
  23. end
  24. end
  25. end
  26. end
  27. end

app/actions/users/new.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Users
  5. # The new action.
  6. 1 class New < Action
  7. 1 include Deps[:htmx_layout, status_repository: "repositories.user_status"]
  8. 1 def handle request, response
  9. 3 response.render view, statuses: status_repository.all, layout: htmx_layout.call(request)
  10. end
  11. end
  12. end
  13. end
  14. end

app/actions/users/show.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Users
  5. # The show action.
  6. 1 class Show < Action
  7. 1 include Deps[:htmx_layout, repository: "repositories.user"]
  8. 2 params { required(:id).filled :integer }
  9. 1 def handle request, response
  10. 3 parameters = request.params
  11. 3 else: 2 then: 1 halt :unprocessable_content unless parameters.valid?
  12. 2 response.render view,
  13. user: repository.find(parameters[:id]),
  14. layout: htmx_layout.call(request)
  15. end
  16. end
  17. end
  18. end
  19. end

app/actions/users/update.rb

100.0% lines covered

100.0% branches covered

13 relevant lines. 13 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Actions
  4. 1 module Users
  5. # The update action.
  6. 1 class Update < Action
  7. 1 include Deps[
  8. updater: "aspects.users.updater",
  9. repository: "repositories.user",
  10. status_repository: "repositories.user_status",
  11. show_view: "views.users.show"
  12. ]
  13. 1 def handle request, response
  14. 2 in: 1 case updater.call(**request.params.to_h)
  15. 1 in: 1 in Success(user) then save user, response
  16. 1 in Failure(result) then error request, result, response
  17. skipped # :nocov:
  18. skipped # :nocov:
  19. end
  20. end
  21. 1 private
  22. 1 def save(user, response) = response.render show_view, user:, layout: false
  23. 1 def error request, result, response
  24. 1 response.render view,
  25. user: repository.find(request.params[:id]),
  26. statuses: status_repository.all,
  27. fields: result[:user],
  28. errors: result.errors[:user],
  29. layout: false
  30. end
  31. end
  32. end
  33. end
  34. end

app/aspects/croner.rb

100.0% lines covered

100.0% branches covered

43 relevant lines. 43 lines covered and 0 lines missed.
20 total branches, 20 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 require "core"
  4. 1 require "functionable"
  5. 1 module Terminus
  6. 1 module Aspects
  7. # Parses values into cron format.
  8. 1 module Croner
  9. 1 extend Functionable
  10. 1 def call interval = nil, unit = "minute", time: Time.utc(2025, 1, 1, 0, 0, 0)
  11. 34 when: 13 case unit
  12. 13 when: 3 when "minute" then for_minute interval, time
  13. 3 when: 3 when "hour" then for_hour interval, time
  14. 3 when: 5 when "day" then for_day interval, time
  15. 5 when: 7 when "week" then for_week interval, time
  16. 7 when: 2 when "month" then for_month interval, time
  17. 2 else: 1 when "none" then Core::EMPTY_STRING
  18. 1 else fail ArgumentError, "Unknown unit: #{unit.inspect}."
  19. end
  20. end
  21. 1 def for_minute interval, time
  22. 13 zone = time.zone
  23. 13 then: 5 else: 8 interval ? "*/#{interval} * * * * #{zone}" : "* * * * * #{zone}"
  24. end
  25. 1 def for_hour interval, time
  26. 3 _, minute, *, zone = time.to_a
  27. 3 in: 2 case [interval, time]
  28. 2 else: 1 in Integer, Time then "#{minute} */#{interval} * * * #{zone}"
  29. 1 else "#{minute} * * * * #{zone}"
  30. end
  31. end
  32. 1 def for_day interval, time
  33. 3 _, minute, hour, *, zone = time.to_a
  34. 3 in: 2 case [interval, time]
  35. 2 else: 1 in Integer, Time then "#{minute} #{hour} */#{interval} * * #{zone}"
  36. 1 else "#{minute} #{hour} * * * #{zone}"
  37. end
  38. end
  39. 1 def for_week interval, time
  40. 5 _, minute, hour, *, zone = time.to_a
  41. 5 in: 1 case interval
  42. 1 in: 3 in Integer then "#{minute} #{hour} * * #{interval} #{zone}"
  43. 3 else: 1 in Array then %(#{minute} #{hour} * * #{interval.join ","} #{zone})
  44. 1 else "#{minute} #{hour} * * 0 #{zone}"
  45. end
  46. end
  47. 1 def for_month interval, time
  48. 7 _, minute, hour, *, zone = time.to_a
  49. 7 in: 2 case [interval, time]
  50. 2 in Integer, Time then "#{minute} #{hour} * */#{interval} * #{zone}"
  51. in: 2 in String, Time
  52. 2 part, directive = interval.scan(/\d+|\D+/)
  53. 2 in: 2 %(#{minute} #{hour} #{directive} */#{part} * #{zone})
  54. 2 else: 1 in Array, Time then %(#{minute} 0 #{interval.join ","} * * #{zone})
  55. 1 else "#{minute} #{hour} 1 * * #{zone}"
  56. end
  57. end
  58. 1 conceal %i[for_minute for_hour for_day for_week for_month]
  59. end
  60. end
  61. end

app/aspects/devices/defaulter.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "securerandom"
  3. 1 module Terminus
  4. 1 module Aspects
  5. 1 module Devices
  6. # Builds default attributes for new devices.
  7. 1 class Defaulter
  8. 1 def initialize randomizer: SecureRandom, mac_address_builder: Devices::MACAddressBuilder
  9. 40 @randomizer = randomizer
  10. 40 @mac_address_builder = mac_address_builder
  11. end
  12. 1 def call
  13. {
  14. 33 api_key: randomizer.alphanumeric(20),
  15. mac_address: mac_address_builder.call,
  16. firmware_update: true,
  17. friendly_id: randomizer.hex(3).upcase,
  18. image_timeout: 0,
  19. label: "TRMNL",
  20. refresh_rate: 900
  21. }
  22. end
  23. 1 private
  24. 1 attr_reader :randomizer, :mac_address_builder
  25. end
  26. end
  27. end
  28. end

app/aspects/devices/mac_address_builder.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "securerandom"
  3. 1 module Terminus
  4. 1 module Aspects
  5. 1 module Devices
  6. # Builds a random, locally administered, unicast MAC address.
  7. 1 MACAddressBuilder = lambda do |randomizer: SecureRandom|
  8. 47 zero_mask = 0xFC # 11111100
  9. 47 local_mask = 0x02 # 00000010
  10. 47 bytes = randomizer.bytes(6).unpack "C*"
  11. 47 bytes[0] = (bytes[0] & zero_mask) | local_mask
  12. 329 bytes.map { format "%02X", it }
  13. .join ":"
  14. end
  15. end
  16. end
  17. end

app/aspects/devices/provisioner.rb

100.0% lines covered

100.0% branches covered

33 relevant lines. 33 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "pipeable"
  4. 1 module Terminus
  5. 1 module Aspects
  6. 1 module Devices
  7. # Handles the setup and default configuration of new devices.
  8. 1 class Provisioner
  9. 1 include Deps[
  10. "aspects.devices.defaulter",
  11. "aspects.screens.welcomer",
  12. repository: "repositories.device",
  13. playlist_repository: "repositories.playlist",
  14. item_repository: "repositories.playlist_item"
  15. ]
  16. 1 include Dry::Monads[:result]
  17. 1 include Pipeable
  18. 1 def call(mac_address: MACAddressBuilder.call, **)
  19. 30 device = repository.find_by(mac_address:)
  20. 30 then: 2 else: 28 return Success device if device
  21. 28 process(mac_address, **)
  22. end
  23. 1 private
  24. 1 def process(mac_address, **)
  25. 28 cached_device = nil
  26. 28 pipe(
  27. create(mac_address, **),
  28. 24 fmap { cached_device = it },
  29. 24 bind { |device| welcomer.call device },
  30. 24 fmap { |screen| configure cached_device, screen }
  31. )
  32. end
  33. 1 def create(mac_address, **)
  34. 28 Success repository.create(defaulter.call.merge!(mac_address:, **))
  35. rescue ROM::SQL::NotNullConstraintError => error
  36. 2 Failure "#{error.message.match(/ERROR: (.+)\n/)[1].capitalize}."
  37. rescue ROM::SQL::ForeignKeyConstraintError => error
  38. 2 Failure error.message.sub(/.+DETAIL: /m, "").strip
  39. end
  40. 1 def configure device, screen
  41. 24 playlist_id = create_playlist_id device
  42. 24 item = item_repository.create_with_position playlist_id:, screen_id: screen.id
  43. 24 playlist_repository.update playlist_id, current_item_id: item.id
  44. 24 repository.update device.id, playlist_id:
  45. end
  46. 1 def create_playlist_id device
  47. 24 id = device.friendly_id
  48. 24 playlist = playlist_repository.create label: "Device #{id}", name: "device_#{id.downcase}"
  49. 24 playlist.id
  50. end
  51. end
  52. end
  53. end
  54. end

app/aspects/devices/sensors/synchronizer.rb

100.0% lines covered

100.0% branches covered

44 relevant lines. 44 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 require "dry/monads"
  4. 1 require "initable"
  5. 1 require "refinements/array"
  6. 1 require "refinements/hash"
  7. 1 module Terminus
  8. 1 module Aspects
  9. 1 module Devices
  10. 1 module Sensors
  11. # Creates or updates device sensor records based on hardware readings.
  12. 1 class Synchronizer
  13. 1 include Deps[
  14. :settings,
  15. :logger,
  16. device_relation: "relations.device",
  17. sensor_repository: "repositories.device_sensor"
  18. ]
  19. 1 include Dry::Monads[:result]
  20. 1 include Initable[schema: proc { Terminus::Schemas::Devices::Sensors::Upsert }]
  21. 1 using Refinements::Array
  22. 1 using Refinements::Hash
  23. 9 def call = load.then { |data| process_devices data }
  24. 1 private
  25. 1 def load
  26. 8 path = settings.sensors_path
  27. 8 then: 5 else: 3 return JSON path.read if path.exist?
  28. 6 logger.debug { "Sensors path not found: #{path}. Skipped." }
  29. 3 Core::EMPTY_HASH
  30. end
  31. 1 def process_devices data
  32. 8 device_relation.select(:id)
  33. 6 .map { it[:id] }
  34. 6 .each { |id| process_sensors id, data }
  35. end
  36. 1 def process_sensors device_id, data
  37. 6 data.fetch("data", Core::EMPTY_ARRAY).map do |entry|
  38. 7 result = schema.call entry
  39. 7 then: 5 if result.success?
  40. 5 deduplicate device_id, result.to_h
  41. else: 2 else
  42. 2 log_error result
  43. end
  44. end
  45. end
  46. 1 def deduplicate device_id, attributes
  47. 5 then: 2 if find_with device_id, attributes
  48. 4 logger.debug(tags: [attributes]) { "Duplicate sensor detected. Skipped." }
  49. else: 3 else
  50. 3 sensor_repository.create device_id:, source: "server", **attributes
  51. end
  52. end
  53. 1 def find_with device_id, attributes
  54. 10 attributes.transform_value!(:created_at) { Time.at(it).utc }
  55. 5 make, model, kind, created_at = attributes.values_at :make, :model, :kind, :created_at
  56. 5 sensor_repository.find_by device_id:,
  57. make:,
  58. model:,
  59. kind:,
  60. created_at:,
  61. source: "device"
  62. end
  63. 1 def log_error result
  64. 2 message = result.errors
  65. .to_h
  66. 4 .map { |key, value| "#{key} #{value.to_sentence}" }
  67. .to_sentence delimiter: "; "
  68. 4 logger.error { "Unable to validate sensor: #{message}." }
  69. end
  70. end
  71. end
  72. end
  73. end
  74. end

app/aspects/devices/synchronizer.rb

100.0% lines covered

100.0% branches covered

15 relevant lines. 15 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "pipeable"
  4. 1 module Terminus
  5. 1 module Aspects
  6. 1 module Devices
  7. # Updates device based on firmware header information.
  8. 1 class Synchronizer
  9. 1 include Deps[
  10. :settings,
  11. firmware_parser: "aspects.firmware.headers.parser",
  12. repository: "repositories.device"
  13. ]
  14. 1 include Pipeable
  15. 1 include Dry::Monads[:result]
  16. 1 def call(headers) = pipe firmware_parser.call(headers), :update
  17. 1 private
  18. 1 def update result, at: Time.now
  19. 11 result.bind do |payload|
  20. 10 device = repository.update_by_mac_address payload.mac_address,
  21. **payload.device_attributes,
  22. synced_at: at
  23. 10 then: 7 else: 3 device ? Success(device) : Failure("Unable to find device by MAC address.")
  24. end
  25. end
  26. end
  27. end
  28. end
  29. end

app/aspects/downloader.rb

100.0% lines covered

100.0% branches covered

20 relevant lines. 20 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 module Terminus
  4. 1 module Aspects
  5. # A simple content downloader.
  6. 1 class Downloader
  7. 1 include Deps[:http, :logger]
  8. 1 include Dry::Monads[:result]
  9. 16 def call(uri) = get(uri).tap { log it, uri }
  10. 1 private
  11. 1 def get uri
  12. 15 http.get(uri).then do |response|
  13. 8 then: 6 else: 2 response.status.success? ? Success(response) : Failure(response)
  14. end
  15. rescue HTTP::RequestError, OpenSSL::SSL::SSLError => error
  16. 6 Failure error.message
  17. end
  18. 1 def log result, uri
  19. 15 in: 6 case result
  20. 12 in: 2 in Success then logger.info { "Downloaded: #{uri}." }
  21. 2 in: 6 in Failure(HTTP::Response => response) then log_error response.body
  22. 6 else: 1 in Failure(String => message) then log_error message
  23. 1 else log_error "Unable to download: #{uri.inspect}."
  24. end
  25. 15 result
  26. end
  27. 10 def log_error(message) = logger.error { message }
  28. end
  29. end
  30. end

app/aspects/extensions/cloner.rb

100.0% lines covered

100.0% branches covered

32 relevant lines. 32 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 module Terminus
  4. 1 module Aspects
  5. 1 module Extensions
  6. # Clones an existing extension.
  7. 1 class Cloner
  8. 1 include Deps[
  9. "aspects.jobs.schedule",
  10. repository: "repositories.extension",
  11. exchange_repository: "repositories.extension_exchange"
  12. ]
  13. 1 include Dry::Monads[:result]
  14. 1 def call(id, **overrides)
  15. 10 Success create(id, build_attributes(id, overrides))
  16. rescue ROM::SQL::UniqueConstraintError => error
  17. 3 build_failure error.message
  18. end
  19. 1 private
  20. 1 def build_attributes id, overrides
  21. 10 original = repository.find id
  22. 10 {
  23. **original.to_h.except(:id, :created_at, :updated_at),
  24. label: "#{original.label} Clone",
  25. name: "#{original.name}_clone",
  26. **overrides
  27. }
  28. end
  29. 1 def create id, attributes
  30. 10 extension = create_with_models attributes
  31. 7 add_devices extension, attributes
  32. 7 add_exchanges id, extension
  33. 7 add_schedule extension
  34. 7 extension
  35. end
  36. 1 def create_with_models attributes
  37. 10 repository.create_with_models attributes, Array(attributes[:model_ids])
  38. end
  39. 1 def add_devices extension, attributes
  40. 7 repository.update_with_devices extension.id, {}, Array(attributes[:device_ids])
  41. end
  42. 1 def add_exchanges original_id, extension
  43. 7 exchange_repository.where(extension_id: original_id).each do |original|
  44. 1 exchange_repository.create extension_id: extension.id,
  45. **original.to_h.except(
  46. :id,
  47. :extension_id,
  48. :created_at,
  49. :updated_at
  50. )
  51. end
  52. end
  53. 1 def add_schedule extension
  54. 7 schedule.upsert(*extension.to_schedule)
  55. end
  56. 1 def build_failure message
  57. 3 match = message.match(/Key \((?<key>[^)]+)\)/)
  58. 3 Failure match[:key].to_sym => ["must be unique"]
  59. end
  60. end
  61. end
  62. end
  63. end

app/aspects/extensions/contextualizer.rb

100.0% lines covered

100.0% branches covered

14 relevant lines. 14 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "refinements/hash"
  3. 1 module Terminus
  4. 1 module Aspects
  5. 1 module Extensions
  6. # Assembles the Liquid context for rendering screens.
  7. 1 class Contextualizer
  8. 1 include Deps["aspects.models.finder", sensor_repository: "repositories.device_sensor"]
  9. 1 using Refinements::Hash
  10. 1 def call extension, model_id: nil, device_id: nil
  11. {
  12. 40 "extension" => extension.liquid_attributes.merge!(
  13. "css_classes" => build_screen_classes(model_id, device_id)
  14. ),
  15. "sensors" => load_sensors(device_id)
  16. }
  17. end
  18. 1 private
  19. 1 def build_screen_classes model_id, device_id
  20. 40 model = finder.call(model_id:, device_id:).value_or(nil)
  21. 40 then: 16 else: 24 model.css_classes if model
  22. end
  23. 1 def load_sensors(device_id) = sensor_repository.where(device_id:).map(&:liquid_attributes)
  24. end
  25. end
  26. end
  27. end

app/aspects/extensions/curler.rb

100.0% lines covered

100.0% branches covered

24 relevant lines. 24 lines covered and 0 lines missed.
8 total branches, 8 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 require "initable"
  4. 1 module Terminus
  5. 1 module Aspects
  6. 1 module Extensions
  7. # Renders curl command for exchange and associated data.
  8. 1 class Curler
  9. 1 include Deps["aspects.extensions.uri_builder"]
  10. 1 include Initable[json_formatter: proc { Terminus::Aspects::JSONFormatter }]
  11. 1 def self.render_request verb, uri
  12. 9 then: 8 else: 1 verb.include?("get") ? "curl #{uri}" : "curl --request #{verb.upcase} #{uri}"
  13. end
  14. 1 def self.render_headers attributes
  15. 9 then: 5 else: 4 return if Hash(attributes).empty?
  16. 10 attributes.map { |key, value| "--header '#{key.downcase}: #{value}'" }
  17. end
  18. 1 def call extension, exchange
  19. 9 uri_builder.call(extension, exchange.template)
  20. 9 .map { |uri| render uri, exchange }
  21. .join "\n"
  22. end
  23. 1 private
  24. 1 def render uri, exchange
  25. 9 klass = self.class
  26. [
  27. 9 klass.render_request(exchange.verb, uri),
  28. *klass.render_headers(exchange.headers),
  29. render_body(exchange.body)
  30. ].compact
  31. .each
  32. .with_index
  33. 19 then: 9 else: 10 .map { |line, index| index.zero? ? line : " #{line}" }
  34. .join " \\\n"
  35. end
  36. 1 def render_body attributes
  37. 9 then: 5 else: 4 return if Hash(attributes).empty?
  38. 4 "--data $'#{json_formatter.call attributes}'"
  39. end
  40. end
  41. end
  42. end
  43. end

app/aspects/extensions/defaults.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Aspects
  4. 1 module Extensions
  5. DEFAULTS = {
  6. 1 tags: [],
  7. mode: "light",
  8. kind: "poll",
  9. verb: "get",
  10. start_at: Time.now.strftime("%Y-%m-%dT00:00:00"),
  11. days: [],
  12. interval: 1,
  13. template: <<~BODY
  14. <div class="{{extension.css_classes}}">
  15. <div class="view view--full">
  16. <div class="layout layout--col">
  17. </div>
  18. </div>
  19. </div>
  20. BODY
  21. }.freeze
  22. end
  23. end
  24. end

app/aspects/extensions/exchanges/coalescer.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Aspects
  4. 1 module Extensions
  5. 1 module Exchanges
  6. # Answers coalesced (and sequenced) data for all exchanges.
  7. 1 Coalescer = lambda do |exchanges, index: 1|
  8. 26 exchanges.each.with_object({}) do |exchange, all|
  9. 9 exchange.data.each do |key, value|
  10. 13 all[key.sub(/\d+/, index.to_s)] = value
  11. 13 index += 1
  12. end
  13. end
  14. end
  15. end
  16. end
  17. end
  18. end

app/aspects/extensions/exchanges/refresher.rb

100.0% lines covered

100.0% branches covered

33 relevant lines. 33 lines covered and 0 lines missed.
5 total branches, 5 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/core"
  3. 1 require "dry/monads"
  4. 1 require "initable"
  5. 1 module Terminus
  6. 1 module Aspects
  7. 1 module Extensions
  8. 1 module Exchanges
  9. # Updates an exchange based on multiple responses.
  10. 1 class Refresher
  11. 1 include Deps[
  12. "aspects.extensions.uri_builder",
  13. fetcher: "aspects.extensions.fetchers.sole",
  14. extension_repository: "repositories.extension",
  15. exchange_repository: "repositories.extension_exchange"
  16. ]
  17. 1 include Initable[input: Fetchers::Input]
  18. 1 include Dry::Monads[:result]
  19. 1 def call exchange
  20. 7 extension_id = exchange.extension_id
  21. 7 extension = extension_repository.find extension_id
  22. 7 else: 6 then: 1 return Failure "Unable to find extension by ID: #{extension_id}." unless extension
  23. 6 update exchange, build_inputs(exchange, extension)
  24. end
  25. 1 private
  26. 1 def build_inputs exchange, extension
  27. 6 uri_builder.call(extension, exchange.template).map do |uri|
  28. 7 input[uri:, **exchange.http_attributes]
  29. end
  30. end
  31. 1 def update exchange, inputs
  32. 6 id = exchange.id
  33. 6 payloads = fetch inputs
  34. 6 exchange_repository.update id, **payloads, refreshed_at: Time.now
  35. 6 Success exchange_repository.find id
  36. end
  37. # :reek:FeatureEnvy
  38. # :reek:TooManyStatements
  39. 1 def fetch inputs, data: {}, errors: {}
  40. 6 inputs.each.with_index 1 do |input, index|
  41. 7 key = "source_#{index}"
  42. 7 in: 3 case fetcher.call input
  43. 3 in: 3 in Success(payload) then data.merge! key => payload[:data]
  44. 3 else: 1 in Failure(payload) then errors.merge! key => payload[:error]
  45. 1 else errors.merge! key => "Unable to fetch, invalid result."
  46. end
  47. end
  48. 6 {data:, errors:}
  49. end
  50. end
  51. end
  52. end
  53. end
  54. end

app/aspects/extensions/exporter.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 module Terminus
  4. 1 module Aspects
  5. 1 module Extensions
  6. # Exports extension attributes for sharing.
  7. 1 class Exporter
  8. 1 include Deps[:settings, exchange_repository: "repositories.extension_exchange"]
  9. 1 include Dry::Monads[:result]
  10. 1 def call extension
  11. 3 exchange_repository.where(extension_id: extension.id)
  12. .map(&:export_attributes)
  13. .then do |exchanges|
  14. 3 Success(
  15. version: settings.git_tag,
  16. **extension.export_attributes,
  17. exchanges:
  18. )
  19. end
  20. end
  21. end
  22. end
  23. end
  24. end

app/aspects/extensions/fetchers/input.rb

100.0% lines covered

100.0% branches covered

8 relevant lines. 8 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 module Terminus
  4. 1 module Aspects
  5. 1 module Extensions
  6. 1 module Fetchers
  7. # The input for HTTP requests.
  8. 1 Input = Data.define :headers, :verb, :uri, :body do
  9. 1 def initialize uri:, headers: Core::EMPTY_HASH, verb: "get", body: Core::EMPTY_HASH
  10. 39 super
  11. end
  12. end
  13. end
  14. end
  15. end
  16. end

app/aspects/extensions/fetchers/sole.rb

100.0% lines covered

100.0% branches covered

42 relevant lines. 42 lines covered and 0 lines missed.
10 total branches, 10 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 require "dry/monads"
  4. 1 require "initable"
  5. 1 module Terminus
  6. 1 module Aspects
  7. 1 module Extensions
  8. 1 module Fetchers
  9. # Processes a single request.
  10. 1 class Sole
  11. 1 include Deps[:http]
  12. 1 include Initable[parser: Extensions::Parser, special_header: "Accept"]
  13. 1 include Dry::Monads[:result]
  14. 1 def call input
  15. 32 process(input).fmap { maybe_alter_mime_type input.headers, it }
  16. 13 .fmap { |mime_type, body| parse mime_type, body }
  17. 13 .bind { build_success input, it }
  18. end
  19. 1 private
  20. 1 def process input
  21. 19 http.headers(input.headers)
  22. .follow
  23. .public_send(input.verb, input.uri)
  24. 15 then: 13 else: 2 .then { it.status.success? ? Success(it) : build_detailed_failure(input, it) }
  25. 1 rescue HTTP::RequestError then build_failure input, "Unable to make request"
  26. 1 rescue HTTP::ConnectionError then build_failure input, "Unable to connect"
  27. 1 rescue HTTP::TimeoutError then build_failure input, "Connection timed out"
  28. 1 rescue OpenSSL::SSL::SSLError then build_failure input, "Unable to secure connection"
  29. end
  30. 1 def maybe_alter_mime_type headers, response
  31. 13 type = headers && headers[special_header]
  32. 13 [type || response.mime_type, response.body]
  33. end
  34. 1 def parse type, body
  35. 13 when: 4 case type
  36. 4 when: 1 when %r(application/([[:alnum:]][\w!#&-^$]*\+)?json) then parser.from_json body
  37. 1 when: 1 when %r(image/.+) then parser.from_image body
  38. 1 when: 1 when "text/csv" then parser.from_csv body
  39. 1 when "text/plain" then parser.from_text body
  40. when: 4 when "text/xml", "application/xml", "application/rss+xml", "application/atom+xml"
  41. 4 else: 2 parser.from_xml body
  42. 2 else Failure "Unknown MIME Type: #{type}."
  43. end
  44. end
  45. # :reek:FeatureEnvy
  46. 1 def build_success input, result
  47. 13 then: 11 if result.success?
  48. 11 Success data: result.success, error: Core::EMPTY_HASH
  49. else: 2 else
  50. 2 build_failure input, result.failure
  51. end
  52. end
  53. 1 def build_failure input, body
  54. 6 Failure data: Core::EMPTY_HASH, error: {uri: input.uri, code: nil, type: nil, body:}
  55. end
  56. # :reek:FeatureEnvy
  57. 1 def build_detailed_failure input, error
  58. 2 Failure data: Core::EMPTY_HASH,
  59. error: {
  60. uri: input.uri,
  61. code: error.code,
  62. type: error.mime_type,
  63. body: error.body
  64. }
  65. end
  66. end
  67. end
  68. end
  69. end
  70. end

app/aspects/extensions/importers/remote/creator.rb

100.0% lines covered

100.0% branches covered

39 relevant lines. 39 lines covered and 0 lines missed.
5 total branches, 5 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 require "dry/monads"
  4. 1 module Terminus
  5. 1 module Aspects
  6. 1 module Extensions
  7. 1 module Importers
  8. 1 module Remote
  9. # Creates extension from plugin (recipe).
  10. 1 class Creator
  11. 1 include Deps[
  12. :logger,
  13. "aspects.extensions.importers.remote.transformer",
  14. keyer: "aspects.extensions.importers.remote.transformers.template_keys",
  15. repository: "repositories.extension",
  16. model_repository: "repositories.model",
  17. exchange_repository: "repositories.extension_exchange"
  18. ]
  19. 1 include Dry::Monads[:result]
  20. 1 def initialize(problem_detail: Aspects::ProblemDetail, **)
  21. 7 super(**)
  22. 7 @problem_detail = problem_detail
  23. end
  24. 1 def call id
  25. 7 transform id
  26. rescue ROM::SQL::UniqueConstraintError => error
  27. 1 Failure problem_detail.duplicate(error.message, nil).detail
  28. end
  29. 1 private
  30. 1 attr_reader :problem_detail
  31. 1 def transform id
  32. 7 transformer.call(id).fmap do |attributes|
  33. 7 record = repository.create_with_models attributes, model_ids
  34. 6 id = record.id
  35. 6 add_exchanges id, attributes
  36. 6 repository.find id
  37. end
  38. end
  39. 1 def model_ids
  40. 7 model_repository.find_by(name: "og_plus").then do |model|
  41. 7 then: 1 else: 6 model ? [model.id] : Core::EMPTY_ARRAY
  42. end
  43. end
  44. 1 def add_exchanges extension_id, attributes
  45. 6 headers, verb, templates, body = attributes.values_at :poll_headers,
  46. :poll_verb,
  47. :poll_template,
  48. :poll_body
  49. 6 templates.each do |content|
  50. 6 template = transform_exchange_template content
  51. 6 exchange_repository.create extension_id:, headers:, verb:, template:, body:
  52. end
  53. end
  54. 1 def transform_exchange_template content
  55. 6 in: 3 case keyer.call content
  56. 3 in Success(content) then content
  57. in: 2 in Failure(message)
  58. 4 logger.debug { message }
  59. 2 Core::EMPTY_STRING
  60. else: 1 else
  61. 2 logger.error { "Unable to transform exchange template." }
  62. 1 Core::EMPTY_STRING
  63. end
  64. end
  65. end
  66. end
  67. end
  68. end
  69. end
  70. end

app/aspects/extensions/importers/remote/extractor.rb

100.0% lines covered

100.0% branches covered

26 relevant lines. 26 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "initable"
  4. 1 require "refinements/pathname"
  5. 1 require "zip"
  6. 1 module Terminus
  7. 1 module Aspects
  8. 1 module Extensions
  9. 1 module Importers
  10. 1 module Remote
  11. # Downloads and decompresses a TRMNL plugin archive.
  12. 1 class Extractor
  13. 1 include Deps["aspects.downloader"]
  14. 1 include Initable[
  15. uri: "https://usetrmnl.com/api/plugin_settings/%<id>s/archive",
  16. client: Zip::File
  17. ]
  18. 1 include Dry::Monads[:result]
  19. 1 using Refinements::Pathname
  20. 1 def call id
  21. 4 format(uri, id:).then { downloader.call it }
  22. 2 .fmap { |response| extract response }
  23. rescue Zip::Error => error
  24. 1 Failure error.message
  25. end
  26. 1 private
  27. 1 def extract response, content: {}
  28. 3 client.open_buffer(response.body.to_s) { |zip| build content, zip }
  29. 1 content
  30. end
  31. 1 def build content, zip
  32. 1 zip.each do |entry|
  33. 6 key = Pathname(entry.name).name.to_s.to_sym
  34. 6 content[key] = entry.get_input_stream.read
  35. end
  36. end
  37. end
  38. end
  39. end
  40. end
  41. end
  42. end

app/aspects/extensions/importers/remote/schema.rb

100.0% lines covered

100.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 module Terminus
  4. 1 module Aspects
  5. 1 module Extensions
  6. 1 module Importers
  7. 1 module Remote
  8. # Defines import schema.
  9. 1 Schema = Dry::Schema.Params do
  10. 1 optional(:custom_fields).array(:hash)
  11. 1 required(:dark_mode).filled :bool
  12. 1 required(:name).filled :string
  13. 1 required(:polling_body).maybe :hash
  14. 1 required(:polling_headers).maybe :hash
  15. 1 required(:polling_url).maybe :string
  16. 1 required(:polling_verb).filled :string
  17. 1 required(:refresh_interval).filled :integer
  18. 1 required(:static_data).maybe :hash
  19. 1 required(:strategy).filled :string
  20. 1 after(:value_coercer, &Schemas::Coercers::JSONToHash.curry[:polling_body])
  21. 1 after(:value_coercer, &Schemas::Coercers::URIQueryToHash.curry[:polling_headers])
  22. 1 after(:value_coercer, &Schemas::Coercers::JSONToHash.curry[:static_data])
  23. end
  24. end
  25. end
  26. end
  27. end
  28. end

app/aspects/extensions/importers/remote/transformer.rb

100.0% lines covered

100.0% branches covered

27 relevant lines. 27 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 module Terminus
  4. 1 module Aspects
  5. 1 module Extensions
  6. 1 module Importers
  7. 1 module Remote
  8. # Transforms remote plugin (recipe) data into extension attributes.
  9. 1 class Transformer
  10. 1 include Deps[
  11. "aspects.extensions.importers.remote.extractor",
  12. "aspects.extensions.importers.remote.transformers.data",
  13. "aspects.extensions.importers.remote.transformers.default",
  14. "aspects.extensions.importers.remote.transformers.keys",
  15. "aspects.extensions.importers.remote.transformers.kind",
  16. "aspects.extensions.importers.remote.transformers.template",
  17. "aspects.extensions.importers.remote.transformers.poll"
  18. ]
  19. 1 include Dry::Monads[:result]
  20. 1 def initialize(schema: Importers::Remote::Schema, **)
  21. 5 @schema = schema
  22. 5 super(**)
  23. end
  24. 6 def call(id) = extractor.call(id).bind { |archive| process archive }
  25. 1 private
  26. 1 attr_reader :schema
  27. 1 def process archive
  28. # Order matters.
  29. 9 validate(archive).bind { |attributes| keys.call attributes.to_h }
  30. 4 .bind { |attributes| poll.call attributes }
  31. 4 .bind { |attributes| kind.call attributes }
  32. 3 .bind { |attributes| data.call attributes }
  33. 3 .bind { |attributes| template.call attributes, archive }
  34. 3 .bind { |attributes| default.call attributes }
  35. end
  36. 1 def validate archive
  37. 5 then: 1 if archive.key? :transform
  38. 1 Failure "Serverless transforms are not supported yet."
  39. else: 4 else
  40. 4 YAML.load(archive[:settings])
  41. 4 .then { |settings| schema.call settings }
  42. .to_monad
  43. end
  44. end
  45. end
  46. end
  47. end
  48. end
  49. end
  50. end

app/aspects/extensions/importers/remote/transformers/data.rb

100.0% lines covered

100.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 require "dry/monads"
  4. 1 require "initable"
  5. 1 require "refinements/hash"
  6. 1 module Terminus
  7. 1 module Aspects
  8. 1 module Extensions
  9. 1 module Importers
  10. 1 module Remote
  11. 1 module Transformers
  12. # Transforms custom field defaults into data defaults.
  13. 1 class Data
  14. 1 include Initable[target: :fields, keys: %w[keyname default]]
  15. 1 include Dry::Monads[:result]
  16. 1 using Refinements::Hash
  17. 1 def call attributes
  18. 6 values = attributes.fetch(target, Core::EMPTY_HASH)
  19. .each
  20. .with_object({}) do |item, all|
  21. 6 key, value = item.values_at(*keys)
  22. 6 then: 5 else: 1 all[key] = value if value
  23. end
  24. 6 Success attributes.merge!(data: {"values" => values}.compress)
  25. end
  26. end
  27. end
  28. end
  29. end
  30. end
  31. end
  32. end

app/aspects/extensions/importers/remote/transformers/default.rb

100.0% lines covered

100.0% branches covered

15 relevant lines. 15 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "initable"
  4. 1 require "refinements/string"
  5. 1 module Terminus
  6. 1 module Aspects
  7. 1 module Extensions
  8. 1 module Importers
  9. 1 module Remote
  10. 1 module Transformers
  11. # Transforms (mutates) by adding defaults for initialization.
  12. 1 class Default
  13. 1 include Initable[description: "Imported from TRMNL.", unit: "minute"]
  14. 1 include Dry::Monads[:result]
  15. 1 using Refinements::String
  16. 1 def call attributes
  17. 5 Success attributes.merge!(
  18. name: attributes[:label].snakecase.tr("/", "_"),
  19. description:,
  20. interval: 1,
  21. unit: "none"
  22. )
  23. end
  24. end
  25. end
  26. end
  27. end
  28. end
  29. end
  30. end

app/aspects/extensions/importers/remote/transformers/keys.rb

100.0% lines covered

100.0% branches covered

17 relevant lines. 17 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "initable"
  4. 1 require "refinements/hash"
  5. 1 module Terminus
  6. 1 module Aspects
  7. 1 module Extensions
  8. 1 module Importers
  9. 1 module Remote
  10. 1 module Transformers
  11. # Transforms (mutates) attributes for initialization.
  12. 1 class Keys
  13. 1 include Dry::Monads[:result]
  14. 1 using Refinements::Hash
  15. 1 include Initable[
  16. map: {
  17. name: :label,
  18. polling_headers: :poll_headers,
  19. polling_verb: :poll_verb,
  20. polling_url: :poll_template,
  21. polling_body: :poll_body,
  22. custom_fields: :fields,
  23. refresh_interval: :interval
  24. },
  25. deletes: %i[dark_mode]
  26. ]
  27. 1 def call attributes
  28. 10 deletes.each { attributes.delete it }
  29. 5 attributes.transform_keys! map
  30. 5 Success attributes
  31. end
  32. end
  33. end
  34. end
  35. end
  36. end
  37. end
  38. end

app/aspects/extensions/importers/remote/transformers/kind.rb

100.0% lines covered

100.0% branches covered

28 relevant lines. 28 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "refinements/array"
  4. 1 module Terminus
  5. 1 module Aspects
  6. 1 module Extensions
  7. 1 module Importers
  8. 1 module Remote
  9. 1 module Transformers
  10. # Transforms (mutates) strategy and polling/static attributes for initialization.
  11. 1 class Kind
  12. 1 include Dry::Monads[:result]
  13. 1 using Refinements::Array
  14. 1 KINDS = {"polling" => "poll", "static" => "static"}.freeze
  15. 1 def initialize kinds: KINDS
  16. 9 @kinds = kinds
  17. 9 @keys = kinds.keys
  18. end
  19. 1 def call attributes
  20. 8 strategy = attributes.delete :strategy
  21. 8 then: 5 if keys.include? strategy
  22. 5 Success process(strategy, attributes)
  23. else: 3 else
  24. 3 Failure "Unsupported kind: #{strategy}. Use: #{keys.to_sentence :or}."
  25. end
  26. end
  27. 1 private
  28. 1 attr_reader :kinds, :keys
  29. 1 def process strategy, attributes
  30. 5 kind = kinds[strategy]
  31. 5 static_data = attributes.delete :static_data
  32. 5 then: 2 if kind == "static"
  33. 2 attributes.merge! kind:, static_body: static_data
  34. else: 3 else
  35. 3 attributes.merge! kind:, poll_body: attributes[:poll_body]
  36. end
  37. end
  38. end
  39. end
  40. end
  41. end
  42. end
  43. end
  44. end

app/aspects/extensions/importers/remote/transformers/poll.rb

100.0% lines covered

100.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "initable"
  4. 1 require "refinements/hash"
  5. 1 module Terminus
  6. 1 module Aspects
  7. 1 module Extensions
  8. 1 module Importers
  9. 1 module Remote
  10. 1 module Transformers
  11. # Transforms poll URIs/templates.
  12. 1 class Poll
  13. 1 include Initable[
  14. key: :poll_template,
  15. line_pattern: /\r\n|\n|\r|\s/,
  16. liquid_pattern: /\{\{.+\}\}/m
  17. ]
  18. 1 include Dry::Monads[:result]
  19. 1 using Refinements::Hash
  20. 1 def call attributes
  21. 9 value = String attributes[key]
  22. 9 then: 2 if value.match? liquid_pattern
  23. 2 attributes[key] = [value]
  24. 2 Success attributes
  25. else: 7 else
  26. 14 Success attributes.transform_value!(key) { value.split line_pattern }
  27. end
  28. end
  29. end
  30. end
  31. end
  32. end
  33. end
  34. end
  35. end

app/aspects/extensions/importers/remote/transformers/template.rb

100.0% lines covered

100.0% branches covered

20 relevant lines. 20 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "initable"
  3. 1 require "refinements/hash"
  4. 1 module Terminus
  5. 1 module Aspects
  6. 1 module Extensions
  7. 1 module Importers
  8. 1 module Remote
  9. 1 module Transformers
  10. # Transforms (mutates) the full Liquid template for initialization.
  11. 1 class Template
  12. 1 include Deps[keyer: "aspects.extensions.importers.remote.transformers.template_keys"]
  13. 1 include Initable[
  14. layout: <<~BODY
  15. <div class="{{extension.css_classes}}">
  16. <div class="view view--full">
  17. %<content>s
  18. </div>
  19. </div>
  20. BODY
  21. ]
  22. 1 using Refinements::Hash
  23. 1 def call attributes, archive
  24. 12 merge_content(archive).then { keyer.call it }
  25. 6 .fmap { attributes.merge! template: it }
  26. end
  27. 1 private
  28. 1 def merge_content archive
  29. 6 archive.use do |shared, full|
  30. 6 full_transform = format layout, content: full
  31. 6 [shared, full_transform].compact.join "\n\n"
  32. end
  33. end
  34. end
  35. end
  36. end
  37. end
  38. end
  39. end
  40. end

app/aspects/extensions/importers/remote/transformers/template_keys.rb

100.0% lines covered

100.0% branches covered

23 relevant lines. 23 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "initable"
  4. 1 module Terminus
  5. 1 module Aspects
  6. 1 module Extensions
  7. 1 module Importers
  8. 1 module Remote
  9. 1 module Transformers
  10. # Transforms a Liquid template to use Terminus keys instead of TRMNL keys.
  11. 1 class TemplateKeys
  12. 1 include Initable[
  13. key_map: {
  14. "rss." => "source_1.rss.",
  15. "source_1.data" => "source_1",
  16. "trmnl.plugin_settings.instance_name" => "extension.label",
  17. "trmnl.plugin_settings.custom_fields_values" => "extension.values",
  18. "trmnl.plugin_settings.custom_fields[0]" => "extension.fields[0]"
  19. },
  20. index_pattern: /
  21. (?<prefix>IDX) # Prefix
  22. _ # Delimiter
  23. (?<index>\d+) # Index
  24. /mx
  25. ]
  26. 1 include Dry::Monads[:result]
  27. 1 def call content
  28. 11 mutation = content.dup
  29. 11 format_sources mutation
  30. 11 format_fields mutation
  31. 11 Success mutation
  32. end
  33. 1 private
  34. 1 def format_sources content, offset: 1
  35. 11 content.gsub! index_pattern do
  36. 9 captures = Regexp.last_match.named_captures
  37. 9 "source_#{captures["index"].to_i + offset}"
  38. end
  39. end
  40. 1 def format_fields content
  41. 66 key_map.each { |original, modification| content.gsub! original, modification }
  42. end
  43. end
  44. end
  45. end
  46. end
  47. end
  48. end
  49. end

app/aspects/extensions/parser.rb

100.0% lines covered

100.0% branches covered

27 relevant lines. 27 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 require "core"
  4. 1 require "csv"
  5. 1 require "dry/monads"
  6. 1 require "functionable"
  7. 1 require "json"
  8. 1 require "nori"
  9. 1 module Terminus
  10. 1 module Aspects
  11. 1 module Extensions
  12. # Parses supported data types into a hash for further processing.
  13. 1 module Parser
  14. 1 extend Dry::Monads[:result]
  15. 1 extend Functionable
  16. 1 def from_csv body
  17. 7 Success ::CSV.parse(String(body), headers: true).each.map(&:to_h)
  18. rescue ::CSV::MalformedCSVError => error
  19. 2 Failure error.message
  20. end
  21. 1 def from_image(body) = Success body
  22. 1 def from_json body
  23. 9 then: 2 else: 7 content = String(body).empty? ? Core::EMPTY_ARRAY : JSON(body)
  24. 8 Success content
  25. rescue ::JSON::ParserError => error
  26. 1 Failure "#{error.message.capitalize}."
  27. end
  28. 1 def from_text body
  29. 6 Success String(body).split
  30. rescue ArgumentError => error
  31. 1 Failure "#{error.message.capitalize}."
  32. end
  33. 1 def from_xml body, nori: Nori.new(parser: :rexml)
  34. 9 content = nori.parse String(body)
  35. 8 then: 2 else: 6 Success content.empty? ? Core::EMPTY_ARRAY : content
  36. rescue REXML::ParseException => error
  37. 1 Failure error.message
  38. end
  39. end
  40. end
  41. end
  42. end

app/aspects/extensions/renderer.rb

100.0% lines covered

100.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "refinements/hash"
  4. 1 module Terminus
  5. 1 module Aspects
  6. 1 module Extensions
  7. # Renders extension based on kind.
  8. 1 class Renderer
  9. 1 include Deps[
  10. "aspects.extensions.contextualizer",
  11. "aspects.extensions.renderers.image",
  12. "aspects.extensions.renderers.poll",
  13. "aspects.extensions.renderers.static"
  14. ]
  15. 1 include Dry::Monads[:result]
  16. 1 using Refinements::Hash
  17. 1 def call extension, model_id: nil, device_id: nil
  18. 16 process extension, contextualizer.call(extension, model_id:, device_id:)
  19. end
  20. 1 private
  21. 1 def process extension, context
  22. 16 kind = extension.kind
  23. 16 when: 1 case kind
  24. 1 when: 11 when "image" then image.call extension, context:
  25. 11 when: 3 when "poll" then poll.call extension, context:
  26. 3 else: 1 when "static" then static.call extension, context:
  27. 1 else Failure "Unsupported extension kind: #{kind}."
  28. end
  29. end
  30. end
  31. end
  32. end
  33. end

app/aspects/extensions/renderers/image.rb

100.0% lines covered

100.0% branches covered

20 relevant lines. 20 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 require "dry/monads"
  4. 1 module Terminus
  5. 1 module Aspects
  6. 1 module Extensions
  7. 1 module Renderers
  8. # Uses Liquid template to render images.
  9. 1 class Image
  10. 1 include Deps[
  11. exchange_repository: "repositories.extension_exchange",
  12. renderer: "liquid.sanitize"
  13. ]
  14. 1 include Dry::Monads[:result]
  15. 1 def call extension, context: Core::EMPTY_HASH
  16. 2 exchanges = exchange_repository.where extension_id: extension.id
  17. 2 then: 1 if exchanges.one?
  18. 1 content = renderer.call(
  19. extension.template,
  20. {**context, "source_1" => {"url" => exchanges.first.template}}
  21. )
  22. 1 Success content
  23. else: 1 else
  24. 1 render_many extension, exchanges, context
  25. end
  26. end
  27. 1 private
  28. 1 def render_many extension, exchanges, context
  29. 1 data = exchanges.each.with_index(1).with_object({}) do |(exchange, index), all|
  30. 2 all["source_#{index}"] = {"url" => exchange.template}
  31. end
  32. 1 Success renderer.call(extension.template, context.merge(data))
  33. end
  34. end
  35. end
  36. end
  37. end
  38. end

app/aspects/extensions/renderers/poll.rb

100.0% lines covered

100.0% branches covered

21 relevant lines. 21 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 require "dry/monads"
  4. 1 require "initable"
  5. 1 module Terminus
  6. 1 module Aspects
  7. 1 module Extensions
  8. 1 module Renderers
  9. # Uses Liquid template to render poll data.
  10. 1 class Poll
  11. 1 include Deps[
  12. "aspects.extensions.exchanges.refresher",
  13. exchange_repository: "repositories.extension_exchange",
  14. renderer: "liquid.sanitize"
  15. ]
  16. 1 include Dry::Monads[:result]
  17. 1 include Initable[coalescer: proc { Terminus::Aspects::Extensions::Exchanges::Coalescer }]
  18. 1 def call extension, context: Core::EMPTY_HASH
  19. 14 refresh extension.id
  20. 14 render extension, context
  21. end
  22. 1 private
  23. 1 def refresh extension_id
  24. 17 exchange_repository.where(extension_id:).each { refresher.call it }
  25. end
  26. 1 def render extension, context
  27. 14 exchanges = exchange_repository.where extension_id: extension.id
  28. 14 data = coalescer.call exchanges
  29. 14 Success renderer.call(extension.template, context.merge(data))
  30. end
  31. end
  32. end
  33. end
  34. end
  35. end

app/aspects/extensions/renderers/static.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 require "dry/monads"
  4. 1 module Terminus
  5. 1 module Aspects
  6. 1 module Extensions
  7. 1 module Renderers
  8. # Uses Liquid template to render static data.
  9. 1 class Static
  10. 1 include Deps[renderer: "liquid.sanitize"]
  11. 1 include Dry::Monads[:result]
  12. 1 def call extension, context: Core::EMPTY_HASH
  13. 3 Success renderer.call(
  14. extension.template,
  15. context.merge("source_1" => extension.static_body)
  16. )
  17. end
  18. end
  19. end
  20. end
  21. end
  22. end

app/aspects/extensions/screen_upserter.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Aspects
  4. 1 module Extensions
  5. # Creates or updates associated screen from Liquid content.
  6. 1 class ScreenUpserter
  7. 1 include Deps[
  8. "aspects.extensions.renderer",
  9. "aspects.screens.upserter",
  10. view: "views.extensions.dynamic"
  11. ]
  12. 1 def call extension, model_id: nil, device_id: nil
  13. 8 renderer.call(extension, model_id:, device_id:)
  14. 8 .fmap { view.call content: it }
  15. .bind do |content|
  16. 8 upserter.call model_id:,
  17. device_id:,
  18. content: String.new(content),
  19. **extension.screen_attributes
  20. end
  21. end
  22. end
  23. end
  24. end
  25. end

app/aspects/extensions/uri_builder.rb

100.0% lines covered

100.0% branches covered

8 relevant lines. 8 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Aspects
  4. 1 module Extensions
  5. # A specialized URI builder based template and data to produce an array of fully formed URIs.
  6. 1 class URIBuilder
  7. 1 include Deps["aspects.extensions.contextualizer", renderer: "liquid.basic"]
  8. 1 def call extension, template
  9. 22 contextualizer.call(extension)
  10. 22 .then { |data| renderer.call(template, data).split }
  11. end
  12. end
  13. end
  14. end
  15. end

app/aspects/firmware/headers/model.rb

100.0% lines covered

100.0% branches covered

15 relevant lines. 15 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "refinements/hash"
  3. 1 module Terminus
  4. 1 module Aspects
  5. 1 module Firmware
  6. 1 module Headers
  7. 1 KEY_MAP = {
  8. HTTP_ACCESS_TOKEN: :api_key,
  9. HTTP_BATTERY_VOLTAGE: :battery_voltage,
  10. HTTP_FW_VERSION: :firmware_version,
  11. HTTP_HEIGHT: :height,
  12. HTTP_HOST: :host,
  13. HTTP_ID: :mac_address,
  14. HTTP_MODEL: :model_name,
  15. HTTP_PERCENT_CHARGED: :battery_charge,
  16. HTTP_REFRESH_RATE: :refresh_rate,
  17. HTTP_RSSI: :wifi,
  18. HTTP_SENSORS: :sensors,
  19. HTTP_UPDATE_SOURCE: :wake_reason,
  20. HTTP_USER_AGENT: :user_agent,
  21. HTTP_WIDTH: :width
  22. }.freeze
  23. # Models the HTTP headers for quick access to attributes.
  24. 1 Model = Struct.new(*KEY_MAP.values) do
  25. 1 using Refinements::Hash
  26. 1 def self.for headers, key_map: KEY_MAP
  27. 32 headers.transform_keys(key_map).then { new(**it) }
  28. end
  29. 1 def initialize(**)
  30. 21 super
  31. 21 freeze
  32. end
  33. 1 def device_attributes
  34. {
  35. 12 battery_charge:,
  36. battery_voltage:,
  37. firmware_version: firmware_version.to_s,
  38. wake_reason:,
  39. wifi:,
  40. width:,
  41. height:
  42. }.compress
  43. end
  44. end
  45. end
  46. end
  47. end
  48. end

app/aspects/firmware/headers/parser.rb

100.0% lines covered

100.0% branches covered

20 relevant lines. 20 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "pipeable"
  3. 1 require "refinements/hash"
  4. 1 module Terminus
  5. 1 module Aspects
  6. 1 module Firmware
  7. 1 module Headers
  8. # Parses firmware HTTP headers into records.
  9. 1 class Parser
  10. 1 include Deps[
  11. :logger,
  12. model_name_transformer: "aspects.firmware.headers.transformers.model_name",
  13. sensors_transformer: "aspects.firmware.headers.transformers.sensors"
  14. ]
  15. 1 include Pipeable
  16. 1 using Refinements::Hash
  17. 1 def initialize(schema: Schemas::Firmware::Header, model: Model, **)
  18. 20 @schema = schema
  19. 20 @model = model
  20. 20 super(**)
  21. end
  22. 1 def call headers
  23. 40 logger.debug(tags: tags(headers)) { "Processing device request headers." }
  24. 20 pipe headers,
  25. validate(schema, as: :to_h),
  26. use(model_name_transformer),
  27. use(sensors_transformer),
  28. to(model, :for)
  29. end
  30. 1 private
  31. 1 attr_reader :schema, :model
  32. 1 def tags(headers) = [headers.slice(*schema.key_map.map(&:name)).symbolize_keys]
  33. end
  34. end
  35. end
  36. end
  37. end

app/aspects/firmware/headers/transformers/model_name.rb

100.0% lines covered

100.0% branches covered

20 relevant lines. 20 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "initable"
  4. 1 module Terminus
  5. 1 module Aspects
  6. 1 module Firmware
  7. 1 module Headers
  8. 1 module Transformers
  9. # Transforms a model name to a name that looked up in the database.
  10. 1 class ModelName
  11. 1 include Deps[:logger]
  12. 1 include Initable[
  13. key: :HTTP_MODEL,
  14. map: {
  15. "og" => "og_plus",
  16. "reTerminal E1001" => "seeed_e1001",
  17. "reTerminal E1002" => "seeed_e1002",
  18. "seeed_esp32c3" => "seeed_e1001",
  19. "seeed_esp32s3" => "seeed_e1002",
  20. "waveshare" => "waveshare_4_26",
  21. "x" => "v2",
  22. "xiao_epaper_display" => "og_plus",
  23. "XTEINK_X4" => "xteink_x4"
  24. },
  25. fallback: "og_plus"
  26. ]
  27. 1 include Dry::Monads[:result]
  28. 1 def call headers
  29. 60 rename(headers[key]).bind { |value| Success headers.merge!(key => value) }
  30. end
  31. 1 private
  32. 1 def rename original
  33. 30 value = String map[original]
  34. 30 else: 6 then: 24 return Success value unless value.empty?
  35. 6 logger.debug do
  36. 6 "Unknown name when transforming #{key} header: #{original.inspect}. " \
  37. "Using fallback: #{fallback}."
  38. end
  39. 6 Success fallback
  40. end
  41. end
  42. end
  43. end
  44. end
  45. end
  46. end

app/aspects/firmware/headers/transformers/sensors.rb

100.0% lines covered

100.0% branches covered

29 relevant lines. 29 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 require "dry/monads"
  4. 1 require "initable"
  5. 1 require "refinements/hash"
  6. 1 module Terminus
  7. 1 module Aspects
  8. 1 module Firmware
  9. 1 module Headers
  10. 1 module Transformers
  11. # Transforms sensors header into an array of records.
  12. 1 class Sensors
  13. 1 include Initable[
  14. key: :HTTP_SENSORS,
  15. source: "device",
  16. delimiters: {line: ",", attribute: ";", pair: "="}
  17. ]
  18. 1 include Dry::Monads[:result]
  19. 1 using Refinements::Hash
  20. 1 def call headers
  21. 20 content = String headers[key]
  22. 20 then: 14 else: 6 return split_lines content, headers if content.include? pair_delimiter
  23. 6 Success headers.merge!(key => Core::EMPTY_ARRAY)
  24. end
  25. 1 private
  26. 1 def split_lines content, headers
  27. 14 content.split(line_delimiter)
  28. 15 .map { split_attributes it }
  29. 14 .then { Success headers.merge! key => it }
  30. end
  31. 1 def split_attributes line
  32. 15 line.split(attribute_delimiter)
  33. 85 .to_h { it.split pair_delimiter }
  34. .merge!(source:)
  35. .symbolize_keys!
  36. 14 .transform_value!(:created_at) { Time.at it.to_i }
  37. end
  38. 1 def line_delimiter = delimiters.fetch :line
  39. 1 def attribute_delimiter = delimiters.fetch :attribute
  40. 1 def pair_delimiter = delimiters.fetch :pair
  41. end
  42. end
  43. end
  44. end
  45. end
  46. end

app/aspects/firmware/log_transformer.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 require "initable"
  4. 1 require "refinements/hash"
  5. 1 module Terminus
  6. 1 module Aspects
  7. 1 module Firmware
  8. # Transforms a raw firmware log into attributes fit for creating a device log record.
  9. 1 class LogTransformer
  10. 1 include Initable[key_map: {id: :external_id}.freeze]
  11. 1 using Refinements::Hash
  12. 1 def call payload
  13. 5 payload.fetch(:logs, Core::EMPTY_HASH).map do |item|
  14. 6 item.transform_keys!(key_map).transform_value!(:created_at) { Time.at it }
  15. end
  16. end
  17. end
  18. end
  19. end
  20. end

app/aspects/firmware/models/setup.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Aspects
  4. 1 module Firmware
  5. 1 module Models
  6. # Models data for API setup responses.
  7. 1 Setup = Struct.new :api_key, :friendly_id, :image_url, :message do
  8. 1 def self.for device
  9. 3 new api_key: device.api_key,
  10. friendly_id: device.friendly_id,
  11. image_url: %(#{Hanami.app[:settings].api_uri}/assets/setup.bmp),
  12. message: "Welcome to Terminus!"
  13. end
  14. 1 def initialize(**)
  15. 7 super
  16. 7 self[:message] ||= "MAC Address not registered."
  17. 7 freeze
  18. end
  19. 1 def to_json(*) = to_h.to_json(*)
  20. end
  21. end
  22. end
  23. end
  24. end

app/aspects/firmware/synchronizer.rb

100.0% lines covered

100.0% branches covered

24 relevant lines. 24 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Aspects
  4. 1 module Firmware
  5. # A firmware attachment synchronizer with Core server.
  6. 1 class Synchronizer
  7. 1 include Deps[:trmnl_api, "aspects.downloader", repository: "repositories.firmware"]
  8. 1 include Dry::Monads[:result]
  9. 1 def initialize(struct: Structs::Firmware.new, **)
  10. 5 @struct = struct
  11. 5 super(**)
  12. end
  13. 1 def call
  14. 5 result = trmnl_api.latest_firmware
  15. 5 in: 4 case result
  16. 7 else: 1 in Success(payload) then download(payload).bind { attach it, payload.version }
  17. 1 else result
  18. end
  19. end
  20. 1 private
  21. 1 attr_reader :struct
  22. 1 def download(payload) = downloader.call payload.url
  23. 1 def attach response, version
  24. 3 record = repository.find_by(version:)
  25. 3 then: 1 else: 2 return Success record if record
  26. 2 struct.upload StringIO.new(response), metadata: {"filename" => "#{version}.bin"}
  27. 2 then: 1 else: 1 struct.valid? ? Success(create(version, struct)) : Failure(struct.errors)
  28. end
  29. 1 def create version, struct
  30. 1 repository.create version: version,
  31. kind: "trmnl",
  32. attachment_data: struct.attachment_attributes
  33. end
  34. end
  35. end
  36. end
  37. end

app/aspects/fonts/synchronizer.rb

100.0% lines covered

100.0% branches covered

28 relevant lines. 28 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "refinements/pathname"
  3. 1 module Terminus
  4. 1 module Aspects
  5. 1 module Fonts
  6. # Synchronizes TRMNL Framework fonts for local use.
  7. 1 class Synchronizer
  8. 1 include Deps[:settings, "aspects.downloader"]
  9. 1 include Dry::Monads[:result]
  10. 1 using Refinements::Pathname
  11. 1 def initialize(
  12. configuration_path: Hanami.app.root.join("config/fonts.yml"),
  13. root_uri: "https://trmnl.com/fonts",
  14. **
  15. )
  16. 5 @configuration_path = configuration_path
  17. 5 @root_uri = root_uri
  18. 5 super(**)
  19. end
  20. 1 def call
  21. 5 root = settings.fonts_root.make_dir
  22. 5 names = YAML.load_file(configuration_path).fetch "names"
  23. 5 delete_unknown_files root, names
  24. 106 names.map { download_to root, it }
  25. end
  26. 1 private
  27. 1 attr_reader :configuration_path, :root_uri
  28. 1 def delete_unknown_files root, names
  29. 5 root.files
  30. 3 .map { it.basename.to_s }
  31. 5 .then { |locals| locals - names }
  32. 2 .each { root.join(it).delete }
  33. end
  34. 1 def download_to root, name
  35. 101 path = root.join name
  36. 101 then: 1 else: 100 return Success path if path.exist?
  37. 150 downloader.call("#{root_uri}/#{name}").fmap { |response| path.write response.body }
  38. end
  39. end
  40. end
  41. end
  42. end

app/aspects/jobs/schedule.rb

100.0% lines covered

100.0% branches covered

18 relevant lines. 18 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 require "refinements/hash"
  4. 1 module Terminus
  5. 1 module Aspects
  6. 1 module Jobs
  7. # Manages job schedules.
  8. 1 class Schedule
  9. 1 include Deps[:sidekiq]
  10. 1 using Refinements::Hash
  11. 1 def upsert name, configuration = Dry::EMPTY_HASH, old_name: nil
  12. 20 then: 1 else: 19 return if identical? name, configuration
  13. 19 then: 9 if configuration.empty?
  14. 9 delete name
  15. else: 10 else
  16. 10 then: 1 else: 9 delete old_name if old_name && old_name != name
  17. 10 sidekiq.set_schedule name, configuration
  18. end
  19. end
  20. 1 def delete(name) = sidekiq.remove_schedule name
  21. 1 private
  22. 1 def identical? name, configuration
  23. 20 [name, sidekiq.get_schedule(name)].hash == [name, configuration.stringify_keys].hash
  24. end
  25. end
  26. end
  27. end
  28. end

app/aspects/json_formatter.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
3 total branches, 3 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 require "core"
  4. 1 require "functionable"
  5. 1 require "json"
  6. 1 module Terminus
  7. 1 module Aspects
  8. # A simple JSON pretty printer.
  9. 1 module JSONFormatter
  10. 1 extend Functionable
  11. 1 def call data
  12. 90 in: 62 case data
  13. 62 in: 27 in nil | Core::EMPTY_ARRAY | Core::EMPTY_HASH then Core::EMPTY_STRING
  14. 27 else: 1 in Array | Hash then JSON data, indent: " ", space: " ", object_nl: "\n", array_nl: "\n"
  15. 1 else fail TypeError, "Unknown type to format as JSON for: #{data.inspect}."
  16. end
  17. end
  18. end
  19. end
  20. end

app/aspects/logging/rack_adapter.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Aspects
  4. 1 module Logging
  5. # Adapts Cogger Rack middleware for provider registration.
  6. 1 module RackAdapter
  7. 1 module_function
  8. 1 def with logger
  9. 5 @logger ||= logger
  10. 5 self
  11. end
  12. 1 def new application
  13. 4 @application = Cogger::Rack::Logger.new application, {logger: @logger}
  14. end
  15. 1 def call(environment) = @application.call environment
  16. end
  17. end
  18. end
  19. end

app/aspects/models/cloner.rb

100.0% lines covered

100.0% branches covered

18 relevant lines. 18 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 module Terminus
  4. 1 module Aspects
  5. 1 module Models
  6. # Clones an existing model.
  7. 1 class Cloner
  8. 1 include Deps[repository: "repositories.model"]
  9. 1 include Dry::Monads[:result]
  10. 1 def call(id, **overrides)
  11. 6 original = repository.find id
  12. 6 attributes = {label: "#{original.label} Clone", name: "#{original.name}_clone"}
  13. 6 Success create(original, attributes, overrides)
  14. rescue ROM::SQL::UniqueConstraintError => error
  15. 3 build_failure error.message
  16. end
  17. 1 private
  18. 1 def create original, attributes, overrides
  19. 6 repository.create(
  20. **original.to_h.except(:id, :created_at, :updated_at),
  21. **attributes,
  22. **overrides
  23. )
  24. end
  25. 1 def build_failure message
  26. 3 match = message.match(/Key \((?<key>[^)]+)\)/)
  27. 3 Failure match[:key].to_sym => ["must be unique"]
  28. end
  29. end
  30. end
  31. end
  32. end

app/aspects/models/defaults.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Aspects
  4. 1 module Models
  5. 1 DEFAULTS = {
  6. mime_type: "image/png",
  7. colors: 2,
  8. bit_depth: 1,
  9. rotation: 0,
  10. offset_x: 0,
  11. offset_y: 0,
  12. scale_factor: 1,
  13. width: 0,
  14. height: 0,
  15. css: nil
  16. }.freeze
  17. end
  18. end
  19. end

app/aspects/models/finder.rb

100.0% lines covered

100.0% branches covered

16 relevant lines. 16 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 module Terminus
  4. 1 module Aspects
  5. 1 module Models
  6. # Finds model record by model or device ID.
  7. 1 class Finder
  8. 1 include Deps[
  9. model_repository: "repositories.model",
  10. device_repository: "repositories.device"
  11. ]
  12. 1 include Dry::Monads[:result]
  13. 1 def call model_id: nil, device_id: nil
  14. 108 model = find model_id, device_id
  15. 108 then: 79 else: 29 return Success model if model
  16. 29 Failure "Unable to find model for model ID (#{model_id.inspect}) or " \
  17. "device ID (#{device_id.inspect})."
  18. end
  19. 1 private
  20. 1 def find model_id, device_id
  21. 108 else: 9 then: 99 return model_repository.find model_id unless device_id
  22. 9 device = device_repository.find device_id
  23. 9 then: 8 else: 1 model_repository.find device.model_id if device
  24. end
  25. end
  26. end
  27. end
  28. end

app/aspects/models/palette_optioner.rb

100.0% lines covered

100.0% branches covered

15 relevant lines. 15 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 module Terminus
  4. 1 module Aspects
  5. 1 module Models
  6. # Builds palette selections for use as HTML select options.
  7. 1 class PaletteOptioner
  8. 1 include Deps[
  9. join_repository: "repositories.model_palette",
  10. palette_repository: "repositories.palette"
  11. ]
  12. 1 def call model = nil, prompt: "Select..."
  13. 28 then: 13 else: 1 load_restricted(model).then { it.empty? ? load_all : it }
  14. .reduce [[prompt, ""]] do |all, palette|
  15. 5 all.append [palette.label, palette.id]
  16. end
  17. end
  18. 1 private
  19. 1 def load_restricted model
  20. 14 else: 9 then: 5 return Core::EMPTY_ARRAY unless model
  21. 9 join_repository.where(model_id: model.id)
  22. .map(&:palette_id)
  23. 9 .then { |ids| palette_repository.where id: ids }
  24. end
  25. 1 def load_all = palette_repository.all
  26. end
  27. end
  28. end
  29. end

app/aspects/models/synchronizer.rb

100.0% lines covered

100.0% branches covered

42 relevant lines. 42 lines covered and 0 lines missed.
8 total branches, 8 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "initable"
  3. 1 module Terminus
  4. 1 module Aspects
  5. 1 module Models
  6. # A models synchronizer with Core server.
  7. 1 class Synchronizer
  8. 1 include Deps[
  9. :trmnl_api,
  10. model_repository: "repositories.model",
  11. palette_repository: "repositories.palette",
  12. join_repository: "repositories.model_palette"
  13. ]
  14. 1 include Initable[kinds: %w[byod kindle tidbyt trmnl]]
  15. 1 include Dry::Monads[:result]
  16. 1 def call
  17. 15 result = trmnl_api.models
  18. 15 case result
  19. in: 14 in Success(*payload)
  20. 14 delete payload.map(&:name)
  21. 24 else: 1 payload.each { |item| process item, palette_repository.all }
  22. 1 else result
  23. end
  24. end
  25. 1 private
  26. 1 def delete remote_names
  27. 14 locals = model_repository.where kind: kinds
  28. 14 local_names = locals.map(&:name)
  29. 14 model_repository.delete_all kind: kinds, name: local_names - remote_names
  30. end
  31. 1 def process item, palettes
  32. 10 attributes = item.to_h
  33. 10 names = attributes[:palette_names]
  34. 10 model = upsert item, attributes
  35. 10 add_missing_palettes names, palettes, model
  36. 10 set_default_palette model, names
  37. end
  38. 1 def upsert item, attributes
  39. 10 record = model_repository.find_by name: item.name
  40. 10 then: 7 if record
  41. 7 model_repository.update(record.id, **attributes)
  42. else: 3 else
  43. 3 model_repository.create(**attributes)
  44. end
  45. end
  46. # :reek:TooManyStatements
  47. 1 def add_missing_palettes names, all, model
  48. 10 model_id = model.id
  49. 28 required_ids = all.select { names.include? it.name }
  50. .map(&:id)
  51. 10 existing_ids = join_repository.where(model_id:).map(&:palette_id)
  52. 10 (required_ids - existing_ids).each do |palette_id|
  53. 10 join_repository.create model_id:, palette_id:
  54. end
  55. 10 model
  56. end
  57. 1 def set_default_palette model, names
  58. 10 then: 3 else: 7 return if model.default_palette_id
  59. 7 palette = palette_repository.find_by name: names.last
  60. 7 else: 6 then: 1 return unless palette
  61. 6 model_repository.update model.id, default_palette_id: palette.id
  62. end
  63. end
  64. end
  65. end
  66. end

app/aspects/palettes/synchronizer.rb

100.0% lines covered

100.0% branches covered

31 relevant lines. 31 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "initable"
  3. 1 require "refinements/hash"
  4. 1 module Terminus
  5. 1 module Aspects
  6. 1 module Palettes
  7. # A palettes synchronizer with Core server.
  8. 1 class Synchronizer
  9. 1 include Deps[:trmnl_api, repository: "repositories.palette"]
  10. 1 include Initable[default_kind: "trmnl"]
  11. 1 include Dry::Monads[:result]
  12. 1 using Refinements::Hash
  13. 1 def call
  14. 5 result = trmnl_api.palettes
  15. 5 case result
  16. in: 4 in Success(*payload)
  17. 4 delete payload.map(&:name)
  18. 4 else: 1 process payload
  19. 1 else result
  20. end
  21. end
  22. 1 private
  23. 1 def delete remote_names
  24. 4 locals = repository.where kind: default_kind
  25. 4 local_names = locals.map(&:name)
  26. 4 repository.delete_all kind: default_kind, name: local_names - remote_names
  27. end
  28. 1 def process payload
  29. 7 payload.each { |item| upsert item }
  30. 4 Success()
  31. end
  32. 1 def upsert item
  33. 3 attributes = transform item
  34. 3 record = repository.find_by name: item.name
  35. 3 then: 1 if record
  36. 1 repository.update(record.id, **attributes)
  37. else: 2 else
  38. 2 repository.create(**attributes)
  39. end
  40. end
  41. 4 def transform(item) = item.to_h.then { {**it, kind: default_kind} }
  42. end
  43. end
  44. end
  45. end

app/aspects/password_encryptor.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "bcrypt"
  3. 1 require "initable"
  4. 1 module Terminus
  5. 1 module Aspects
  6. # Creates passwords with computation cost respective of environment.
  7. 1 class PasswordEncryptor
  8. 1 include Initable[
  9. password: BCrypt::Password,
  10. minimum: BCrypt::Engine::MIN_COST,
  11. maximum: BCrypt::Engine::DEFAULT_COST
  12. ]
  13. 1 def call text, environment: Hanami.env
  14. 123 then: 122 else: 1 cost = environment == :test ? minimum : maximum
  15. 123 password.create text, cost:
  16. end
  17. end
  18. end
  19. end

app/aspects/playlists/cloner.rb

100.0% lines covered

100.0% branches covered

30 relevant lines. 30 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 module Terminus
  4. 1 module Aspects
  5. 1 module Playlists
  6. # Clones an existing playlist.
  7. 1 class Cloner
  8. 1 include Deps[
  9. repository: "repositories.playlist",
  10. item_repository: "repositories.playlist_item"
  11. ]
  12. 1 include Dry::Monads[:result]
  13. 1 def call(id, **overrides)
  14. 8 original = repository.with_items.by_pk(id).one
  15. 8 attributes = {label: "#{original.label} Clone", name: "#{original.name}_clone"}
  16. 8 Success create(attributes, overrides, original)
  17. rescue ROM::SQL::UniqueConstraintError => error
  18. 3 build_failure error.message
  19. end
  20. 1 private
  21. 1 def create attributes, overrides, original
  22. 8 repository.create(attributes.merge!(overrides)).tap do |clone|
  23. 5 add_associations clone, original
  24. end
  25. end
  26. 1 def add_associations clone, original
  27. 5 cloned_items = add_items clone, original
  28. 10 then: 3 else: 2 curent_screen_id = original.current_item.then { it.screen_id if it }
  29. 5 else: 3 then: 2 return unless curent_screen_id
  30. 3 add_current_item clone, cloned_items, curent_screen_id
  31. end
  32. 1 def add_items clone, original
  33. 5 original.playlist_items.each.with_index 1 do |item, position|
  34. 8 item_repository.create playlist_id: clone.id, **item.cloneable_attributes, position:
  35. end
  36. end
  37. 1 def add_current_item clone, cloned_items, curent_screen_id
  38. 9 cloned_items.find { |item| item.screen_id == curent_screen_id }
  39. .then do |current_item|
  40. 3 repository.update clone.id, current_item_id: current_item.id
  41. end
  42. end
  43. 1 def build_failure message
  44. 3 match = message.match(/Key \((?<key>[^)]+)\)/)
  45. 3 Failure match[:key].to_sym => ["must be unique"]
  46. end
  47. end
  48. end
  49. end
  50. end

app/aspects/playlists/screen_optioner.rb

100.0% lines covered

100.0% branches covered

8 relevant lines. 8 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Aspects
  4. 1 module Playlists
  5. # Creates list of screen options for selection within a HTML select element.
  6. 1 class ScreenOptioner
  7. 1 include Deps[repository: "repositories.screen"]
  8. 1 def call prompt: "Select..."
  9. 3 repository.all.reduce [[prompt, nil]] do |all, screen|
  10. 2 all.append ["#{screen.label} - #{screen.model.label}", screen.id]
  11. end
  12. end
  13. end
  14. end
  15. end
  16. end

app/aspects/playlists/slide_window.rb

100.0% lines covered

100.0% branches covered

17 relevant lines. 17 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 require "refinements/array"
  4. 1 module Terminus
  5. 1 module Aspects
  6. 1 module Playlists
  7. # The playlist slideshow window of current item and associated slides.
  8. 1 class SlideWindow
  9. 1 include Deps[repository: "repositories.playlist"]
  10. 1 using Refinements::Array
  11. 1 attr_reader :playlist
  12. 1 def initialize(playlist, **)
  13. 18 super(**)
  14. 18 @playlist = playlist
  15. end
  16. 1 def item = playlist.current_item
  17. 1 def screens id = nil
  18. 15 enumerable = playlist.screens.ring
  19. 15 else: 8 then: 7 return enumerable.first unless item
  20. 8 enumerable.find do |before, current, after|
  21. 17 then: 8 else: 9 break before, current, after if current.id == (id || item.screen_id)
  22. end
  23. end
  24. end
  25. end
  26. end
  27. end

app/aspects/problem_detail.rb

100.0% lines covered

100.0% branches covered

20 relevant lines. 20 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 require "functionable"
  4. 1 require "petail"
  5. 1 require "refinements/array"
  6. 1 require "refinements/hash"
  7. 1 module Terminus
  8. 1 module Aspects
  9. # Builds details for global problems.
  10. 1 module ProblemDetail
  11. 1 extend Functionable
  12. 1 using Refinements::Array
  13. 1 using Refinements::Hash
  14. 1 def duplicate message, instance
  15. 3 key, value = message.match(/Key \((?<key>[^)]+)\)=\((?<value>[^)]+)\)/m)
  16. .named_captures
  17. .values_at "key", "value"
  18. 3 Petail[
  19. type: "/problem_details#duplicate_value",
  20. status: :conflict,
  21. detail: "#{key.capitalize} must be unique. " \
  22. "Please use a value other than #{value.inspect}.",
  23. instance:
  24. ]
  25. end
  26. 1 def enum message, instance
  27. 2 key, value = message.match(/"(?<value>.+?)".+:(?<key>.+?)\s/)
  28. .named_captures
  29. .values_at "key", "value"
  30. 2 allowed = JSON(message[/\[".+?"\]/m]).to_usage :or
  31. 2 Petail[
  32. type: "/problem_details#invalid_enum",
  33. status: :unprocessable_content,
  34. detail: "Invalid value for #{key}: #{value.inspect}. Use: #{allowed}.",
  35. instance:
  36. ]
  37. end
  38. 1 def foreign_key message, instance
  39. 2 key, value = message.match(/Key \((?<key>[^)]+)\)=\((?<value>[^)]+)\)/m)
  40. .named_captures
  41. .values_at "key", "value"
  42. 2 Petail[
  43. type: "/problem_details#invalid_foreign_key",
  44. status: :unprocessable_content,
  45. detail: "Invalid `#{key}` value: #{value}. Does not exist.",
  46. instance:
  47. ]
  48. end
  49. end
  50. end
  51. end

app/aspects/sanitizer.rb

100.0% lines covered

100.0% branches covered

18 relevant lines. 18 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "refinements/array"
  3. 1 require "sanitize"
  4. 1 module Terminus
  5. 1 module Aspects
  6. # A custom HTML sanitizer.
  7. 1 class Sanitizer
  8. 1 using Refinements::Array
  9. 1 def initialize configuration_path: Hanami.app.root.join("config/sanitize.yml"),
  10. defaults: Sanitize::Config::RELAXED,
  11. client: Sanitize
  12. 239 @configuration_path = configuration_path
  13. 239 @defaults = defaults
  14. 239 @client = client
  15. end
  16. 1 def call(content) = client.document content, configuration
  17. 1 private
  18. 1 attr_reader :configuration_path, :defaults, :client
  19. 1 def configuration = client::Config.merge(defaults, elements:, attributes:)
  20. 1 def elements
  21. 93 defaults[:elements].including YAML.load_file(configuration_path).fetch("elements")
  22. end
  23. 1 def attributes
  24. 93 defaults[:attributes].merge YAML.load_file(configuration_path).fetch("attributes")
  25. end
  26. end
  27. end
  28. end

app/aspects/screens/converter.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Aspects
  4. 1 module Screens
  5. # Converts to greyscale image based on MIME Type.
  6. 1 class Converter
  7. 1 include Deps["aspects.screens.converters.color", "aspects.screens.converters.monochrome"]
  8. 63 then: 1 else: 61 def call(mold) = mold.color? ? color.call(mold) : monochrome.call(mold)
  9. end
  10. end
  11. end
  12. end

app/aspects/screens/converters/color.rb

100.0% lines covered

100.0% branches covered

28 relevant lines. 28 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Aspects
  4. 1 module Screens
  5. 1 module Converters
  6. # Converts to color image.
  7. 1 class Color
  8. 1 include Deps[mini_magick: "mini_magick.core"]
  9. 1 include Dry::Monads[:result]
  10. 1 def call mold
  11. 6 convert mold
  12. rescue MiniMagick::Error => error
  13. 1 Failure error.message
  14. end
  15. 1 private
  16. 1 def convert mold
  17. 6 output_path = mold.output_path
  18. 24 colors = mold.color_codes.map { "xc:#{it}" }
  19. 6 mini_magick.convert do |tool|
  20. 5 tool << mold.input_path.to_s
  21. 5 then: 1 else: 4 tool.rotate mold.rotation if mold.rotatable?
  22. 5 tool.resize "#{mold.dimensions}!"
  23. 5 then: 1 else: 4 tool.crop mold.crop if mold.cropable?
  24. 5 tool.normalize
  25. 5 tool.modulate "110,150"
  26. 5 tool.colorspace "RGB"
  27. 5 tool.merge! [
  28. "(",
  29. "-size",
  30. "1x1",
  31. *colors,
  32. "+append",
  33. "+write",
  34. "mpr:palette",
  35. "+delete",
  36. ")"
  37. ]
  38. 5 tool.dither "FloydSteinberg"
  39. 5 tool.remap "mpr:palette"
  40. 5 tool.colorspace "sRGB"
  41. 5 tool << "#{mold.file_type}:#{output_path}"
  42. end
  43. 5 Success output_path
  44. end
  45. end
  46. end
  47. end
  48. end
  49. end

app/aspects/screens/converters/monochrome.rb

100.0% lines covered

100.0% branches covered

53 relevant lines. 53 lines covered and 0 lines missed.
10 total branches, 10 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Aspects
  4. 1 module Screens
  5. 1 module Converters
  6. # Converts to monochrome image.
  7. 1 class Monochrome
  8. 1 include Deps[mini_magick: "mini_magick.core"]
  9. 1 include Dry::Monads[:result]
  10. 1 def call mold
  11. 74 route mold
  12. rescue MiniMagick::Error => error
  13. 1 Failure error.message
  14. end
  15. 1 private
  16. 1 def route mold
  17. 74 in: 3 case mold
  18. 3 in: 2 in bit_depth: 1, mode: "dither" then as_one_bit_dither mold
  19. 2 in: 1 in bit_depth: 2..4, mode: "dither" then as_two_to_four_bit_dither mold
  20. 1 in: 65 in bit_depth: 8, mode: "dither" then as_eight_bit_dither mold
  21. 65 in: 2 in bit_depth: 1 then as_one_bit mold
  22. 2 else: 1 in bit_depth: 2..8 then as_two_to_eight_bit mold
  23. 1 else Failure "Unsupported monochrome bit depth: #{mold.bit_depth}."
  24. end
  25. end
  26. 1 def as_one_bit_dither mold
  27. 3 convert mold do |tool|
  28. 3 tool.dither "FloydSteinberg"
  29. 3 tool.remap "pattern:gray50"
  30. end
  31. end
  32. 1 def as_two_to_four_bit_dither mold
  33. 2 convert mold do |tool|
  34. 2 tool.colorspace "Gray"
  35. 2 tool.dither "FloydSteinberg"
  36. 2 tool.posterize mold.grays
  37. end
  38. end
  39. 1 def as_eight_bit_dither mold
  40. 1 convert mold do |tool|
  41. 1 tool.type "Grayscale"
  42. end
  43. end
  44. 1 def as_one_bit mold
  45. 65 convert mold do |tool|
  46. 64 tool.monochrome
  47. 64 tool.colors mold.colors
  48. end
  49. end
  50. 1 def as_two_to_eight_bit mold
  51. 2 convert mold do |tool|
  52. 2 tool.colorspace "Gray"
  53. 2 tool.dither "None"
  54. 2 tool.posterize mold.grays
  55. end
  56. end
  57. 1 def convert mold
  58. 73 output_path = mold.output_path
  59. 73 mini_magick.convert do |tool|
  60. 72 tool << mold.input_path.to_s
  61. 72 then: 1 else: 71 tool.rotate mold.rotation if mold.rotatable?
  62. 72 tool.resize "#{mold.dimensions}!"
  63. 72 then: 1 else: 71 tool.crop mold.crop if mold.cropable?
  64. 72 yield tool
  65. 72 tool.alpha "off"
  66. 72 tool.depth mold.bit_depth
  67. 72 tool.strip
  68. 72 tool << "#{mold.file_type}:#{output_path}"
  69. end
  70. 72 Success output_path
  71. end
  72. end
  73. end
  74. end
  75. end
  76. end

app/aspects/screens/designer/event_stream.rb

100.0% lines covered

100.0% branches covered

27 relevant lines. 27 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 require "initable"
  4. 1 module Terminus
  5. 1 module Aspects
  6. 1 module Screens
  7. 1 module Designer
  8. # Renders device preview image event streams.
  9. 1 class EventStream
  10. 1 include Deps[:assets, :logger, repository: "repositories.screen"]
  11. 1 include Initable[%i[req name], kernel: Kernel]
  12. 1 def each
  13. 5 kernel.loop do
  14. 5 yield <<~CONTENT
  15. event: preview
  16. data: #{load_screen}
  17. CONTENT
  18. 5 kernel.sleep 1
  19. end
  20. end
  21. 1 private
  22. 1 def load_screen
  23. 5 repository.find_by(name:).then do |screen|
  24. 5 then: 3 else: 2 screen ? render_preview(screen) : render_loader
  25. end
  26. end
  27. 1 def render_preview screen
  28. 3 width, height = screen.image_attributes[:metadata].values_at :width, :height
  29. 3 path = screen.image_uri
  30. 3 debug path
  31. 3 %(<img src="#{path}" alt="Preview" class="image" width="#{width}" height="#{height}"/>)
  32. end
  33. 1 def render_loader
  34. 2 path = assets["loader.svg"].path
  35. 2 debug path
  36. 2 %(<img src="#{path}" alt="Loader" class="image" width="800" height="480"/>)
  37. end
  38. 1 def debug path
  39. 10 logger.debug { "Streaming: #{path}." }
  40. end
  41. end
  42. end
  43. end
  44. end
  45. end

app/aspects/screens/designer/middleware.rb

100.0% lines covered

100.0% branches covered

14 relevant lines. 14 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 require "initable"
  4. 1 require_relative "event_stream"
  5. 1 module Terminus
  6. 1 module Aspects
  7. 1 module Screens
  8. 1 module Designer
  9. # Streams Server Side Events (SSE) for device screen previews.
  10. 1 class Middleware
  11. 1 include Initable[
  12. %i[req application],
  13. %i[keyreq pattern],
  14. headers: {
  15. "Content-Encoding" => "identity",
  16. "Content-Type" => "text/event-stream",
  17. "Cache-Control" => "no-cache",
  18. "X-Accel-Buffering" => "no"
  19. },
  20. event_stream: EventStream
  21. ]
  22. 1 def call environment
  23. 306 request = Rack::Request.new environment
  24. 306 path = request.path
  25. 306 in: 2 case path.match pattern
  26. 2 else: 304 in name: then [200, headers, event_stream.new(name)]
  27. 304 else application.call environment
  28. end
  29. end
  30. end
  31. end
  32. end
  33. end
  34. end

app/aspects/screens/fetcher.rb

100.0% lines covered

100.0% branches covered

21 relevant lines. 21 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 module Terminus
  4. 1 module Aspects
  5. 1 module Screens
  6. # Fetches a device's current screen.
  7. 1 class Fetcher
  8. 1 include Deps[
  9. "aspects.screens.sleeper",
  10. playlist_repository: "repositories.playlist",
  11. playlist_item_repository: "repositories.playlist_item"
  12. ]
  13. 1 include Dry::Monads[:result]
  14. 1 def call device
  15. 24 then: 1 if device.asleep?
  16. 1 sleeper.call device
  17. else: 23 else
  18. 31 find_playlist(device.playlist_id).bind { |playlist| find_current_item playlist }
  19. .fmap(&:screen)
  20. end
  21. end
  22. 1 private
  23. 1 def find_playlist id
  24. 23 playlist = playlist_repository.find id
  25. 23 then: 8 else: 15 return Success playlist if playlist
  26. 15 Failure "Unable to fetch screen. Can't find playlist with ID: #{id.inspect}."
  27. end
  28. 1 def find_current_item playlist
  29. 8 id = playlist.current_item_id
  30. 8 item = playlist_item_repository.find id
  31. 8 then: 7 else: 1 return Success item if item
  32. 1 Failure "Unable to fetch screen. Can't find current playlist item with ID: #{id.inspect}."
  33. end
  34. end
  35. end
  36. end
  37. end

app/aspects/screens/find_or_creator.rb

100.0% lines covered

100.0% branches covered

18 relevant lines. 18 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "initable"
  4. 1 module Terminus
  5. 1 module Aspects
  6. 1 module Screens
  7. # Findos or creates record with image attachment from HTML content only.
  8. 1 class FindOrCreator
  9. 1 include Deps[
  10. "aspects.screens.temp_pather",
  11. "aspects.screens.mold_builder",
  12. repository: "repositories.screen"
  13. ]
  14. 1 include Initable[struct: proc { Terminus::Structs::Screen.new }]
  15. 1 include Dry::Monads[:result]
  16. 1 def call(**parameters)
  17. 32 mold_builder.call(**parameters).bind do |mold|
  18. 32 record = find mold
  19. 32 then: 3 else: 29 record ? Success(record) : create(mold)
  20. end
  21. end
  22. 1 private
  23. 1 def find(mold) = repository.find_by name: mold.name, model_id: mold.model_id
  24. 1 def create mold
  25. 29 temp_pather.call mold do |path|
  26. 29 Success repository.create_with_image(path, mold, struct)
  27. end
  28. end
  29. end
  30. end
  31. end
  32. end

app/aspects/screens/gaffer.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Aspects
  4. 1 module Screens
  5. # Creates error with problem details for device.
  6. 1 class Gaffer
  7. 1 include Deps["aspects.screens.upserter", view: "views.screens.gaffe.new"]
  8. 1 def call device, message
  9. 4 upserter.call model_id: device.model_id,
  10. content: String.new(view.call(body: message)),
  11. **device.screen_attributes("error")
  12. end
  13. end
  14. end
  15. end
  16. end

app/aspects/screens/mold.rb

100.0% lines covered

100.0% branches covered

15 relevant lines. 15 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Aspects
  4. 1 module Screens
  5. # Defines the blueprint in which to create a screen.
  6. 1 Mold = Struct.new(
  7. :model_id,
  8. :name,
  9. :label,
  10. :content,
  11. :mode,
  12. :mime_type,
  13. :bit_depth,
  14. :colors,
  15. :color_codes,
  16. :grays,
  17. :rotation,
  18. :offset_x,
  19. :offset_y,
  20. :width,
  21. :height,
  22. :input_path,
  23. :output_path
  24. ) do
  25. 1 def color? = dither? && Array(color_codes).any?
  26. 1 def crop = "#{dimensions}+#{offset_x}+#{offset_y}"
  27. 1 def cropable? = !offset_x.zero? || !offset_y.zero?
  28. 1 def dither? = mode == "dither"
  29. 1 def dimensions = "#{width}x#{height}"
  30. 1 def file_name = %(#{name}.#{mime_type.split("/").last})
  31. 80 then: 1 else: 78 def file_type = mime_type.split("/").last.then { it.match?(/bmp/i) ? "bmp3" : it }
  32. 1 def image? = mime_type.start_with? "image"
  33. 1 def image_attributes = {model_id:, name:, label:}
  34. 1 def rotatable? = !rotation.zero?
  35. 1 def viewport = {width:, height:}
  36. end
  37. end
  38. end
  39. end

app/aspects/screens/mold_builder.rb

100.0% lines covered

100.0% branches covered

27 relevant lines. 27 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "initable"
  4. 1 require "refinements/hash"
  5. 1 module Terminus
  6. 1 module Aspects
  7. 1 module Screens
  8. # Initializes and builds a screen mold.
  9. 1 class MoldBuilder
  10. 1 include Deps["aspects.models.finder", :logger, palette_repository: "repositories.palette"]
  11. 1 include Initable[mold: Mold, fallbacks: {grays: 0, color_codes: []}]
  12. 1 include Dry::Monads[:result]
  13. 1 using Refinements::Hash
  14. 1 def call model_id: nil, device_id: nil, **attributes
  15. 65 finder.call(model_id:, device_id:)
  16. 61 .fmap { |model| palette_attributes_for model }
  17. 61 .fmap { |model, palette| build model, palette, attributes }
  18. 61 .fmap { log_debug it }
  19. end
  20. 1 private
  21. 1 def palette_attributes_for model
  22. 61 palette = palette_repository.find model.default_palette_id
  23. 61 then: 1 else: 60 attributes = palette ? palette.screen_attributes : fallbacks
  24. 61 [model, attributes]
  25. end
  26. 1 def build model, palette_attributes, attributes
  27. 61 allowed_keys = mold.members
  28. 61 mold.new(
  29. **model.to_h.transform_keys!(id: :model_id).slice(*allowed_keys),
  30. **palette_attributes,
  31. **attributes.slice(*allowed_keys)
  32. )
  33. end
  34. 1 def log_debug record
  35. 122 logger.debug(tags: [record.to_h]) { "Screen mold built." }
  36. 61 record
  37. end
  38. end
  39. end
  40. end
  41. end

app/aspects/screens/placeholder.rb

100.0% lines covered

100.0% branches covered

17 relevant lines. 17 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "wholeable"
  3. 1 module Terminus
  4. 1 module Aspects
  5. 1 module Screens
  6. # A fallback (null object) for times when you need a screen that behaves like one but isn't.
  7. 1 class Placeholder
  8. 1 include Wholeable[:id, :label, :name, :uri, :width, :height]
  9. 1 include Deps[:assets]
  10. 1 def initialize(
  11. id: 0,
  12. label: "Placeholder",
  13. name: "placeholder",
  14. uri: "setup.svg",
  15. width: 800,
  16. height: 480,
  17. **
  18. )
  19. 169 @id = id
  20. 169 @label = label
  21. 169 @name = name
  22. 169 @uri = uri
  23. 169 @width = width
  24. 169 @height = height
  25. 169 super(**)
  26. end
  27. 1 def image_uri = assets[uri].path
  28. 1 def popover_attributes = {id:, label:, uri: image_uri, width:, height:}
  29. end
  30. end
  31. end
  32. end

app/aspects/screens/rotator.rb

100.0% lines covered

100.0% branches covered

24 relevant lines. 24 lines covered and 0 lines missed.
8 total branches, 8 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 module Terminus
  4. 1 module Aspects
  5. 1 module Screens
  6. # Updates a device's current playlist item by rotating to next screen.
  7. 1 class Rotator
  8. 1 include Deps[
  9. "aspects.screens.sleeper",
  10. playlist_repository: "repositories.playlist",
  11. item_repository: "repositories.playlist_item"
  12. ]
  13. 1 include Dry::Monads[:result]
  14. 1 def call device
  15. 13 then: 1 if device.asleep?
  16. 1 sleeper.call device
  17. else: 12 else
  18. 21 find_playlist(device.playlist_id).fmap { |playlist| advance_current_item playlist }
  19. 9 .bind { |item| obtain_screen item }
  20. end
  21. end
  22. 1 private
  23. 1 def find_playlist id
  24. 12 playlist = playlist_repository.find id
  25. 12 then: 9 else: 3 return Success playlist if playlist
  26. 3 Failure "Unable to obtain next screen. Can't find playlist with ID: #{id.inspect}."
  27. end
  28. # :reek:FeatureEnvy
  29. 1 def advance_current_item playlist
  30. 9 then: 1 else: 8 return playlist.current_item if playlist.manual?
  31. 8 item_repository.next_item(after: playlist.current_item_position, playlist_id: playlist.id)
  32. 8 .tap { |item| playlist_repository.update_current_item playlist, item }
  33. end
  34. 1 def obtain_screen item
  35. 9 then: 8 else: 1 return Success item.screen if item
  36. 1 Failure "Unable to obtain next screen. Playlist has no items."
  37. end
  38. end
  39. end
  40. end
  41. end

app/aspects/screens/shoter.rb

100.0% lines covered

100.0% branches covered

55 relevant lines. 55 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "ferrum"
  4. 1 require "refinements/pathname"
  5. 1 require "refinements/string"
  6. 1 module Terminus
  7. 1 module Aspects
  8. 1 module Screens
  9. # Saves web page as screenshot.
  10. 1 class Shoter
  11. 1 include Deps[:settings, :logger]
  12. 1 include Dry::Monads[:result]
  13. 1 using Refinements::Pathname
  14. 1 using Refinements::String
  15. 1 OPTIONS = {
  16. "disable-dev-shm-usage" => nil,
  17. "disable-gpu" => nil,
  18. "hide-scrollbar" => nil,
  19. "no-sandbox" => nil
  20. }.freeze
  21. 1 def initialize(browser: Ferrum::Browser, options: OPTIONS, **)
  22. 218 super(**)
  23. 218 @browser = browser
  24. 218 @settings = settings.browser.merge! browser_options: options
  25. end
  26. 1 def call(content, output_path, **viewport) = save content, viewport, output_path
  27. 1 private
  28. 1 attr_reader :settings, :browser
  29. 1 def save content, viewport, output_path
  30. 72 instance = browser.new settings
  31. 69 Pathname.mktmpdir do |work_dir|
  32. 69 instance.create_page
  33. 58 instance.set_viewport(**viewport)
  34. 58 instance.main_frame.content = work_dir.join("content.html").write(content).read
  35. 58 instance.network.wait_for_idle duration: 1
  36. 58 instance.screenshot path: output_path.to_s
  37. 58 instance.quit
  38. end
  39. 58 Success output_path
  40. 3 rescue Ferrum::BrowserError => error then handle_browser_error instance, error
  41. 2 rescue Ferrum::DeadBrowserError => error then handle_dead_browser_error error
  42. 4 rescue Ferrum::TimeoutError => error then handle_timeout_error instance, error
  43. 3 rescue Ferrum::NoSuchTargetError => error then handle_no_such_target_error instance, error
  44. 2 rescue Ferrum::ProcessTimeoutError => error then handle_process_timeout_error error
  45. end
  46. 1 def handle_browser_error instance, error
  47. 3 instance.quit
  48. 6 logger.debug { "Screen shoter has browser error: #{error.message}" }
  49. 3 Failure "Unable to capture screenshot due to an instance error such as " \
  50. "page navigation, element interaction, or something else."
  51. end
  52. 1 def handle_dead_browser_error error
  53. 4 logger.debug { "Screen shoter has dead browser: #{error.message}" }
  54. 2 Failure "Unable to capture screenshot due to a dead browser. " \
  55. "This could mean the browser crashed, server is out of memory, " \
  56. "or a resource limitation has been hit."
  57. end
  58. 1 def handle_timeout_error instance, error
  59. 4 then: 3 else: 1 instance.quit if instance
  60. 8 logger.debug { "Screen shoter has timeout: #{error.message}" }
  61. 4 seconds = settings.fetch :timeout, 0
  62. 4 Failure "Unable to capture screenshot due to timming out after " \
  63. + %(#{seconds} #{"second".pluralize "s"}. ) \
  64. + "This might have happened due to the page taking a long time to load."
  65. end
  66. 1 def handle_no_such_target_error instance, error
  67. 3 instance.quit
  68. 6 logger.debug { "Screen shoter has no such target: #{error.message}" }
  69. 3 Failure "Unable to capture screenshot because the page closed or crashed."
  70. end
  71. 1 def handle_process_timeout_error error
  72. 4 logger.debug { "Screen shoter has process timeout: #{error.message}" }
  73. 2 seconds = settings.fetch :process_timeout, 0
  74. 2 Failure "Unable to capture screenshot because the browser could not produce a " \
  75. + %(websocket URL within #{seconds} #{"second".pluralize "s"}.)
  76. end
  77. end
  78. end
  79. end
  80. end

app/aspects/screens/sleeper.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Aspects
  4. 1 module Screens
  5. # Creates sleep screen for new device.
  6. 1 class Sleeper
  7. 1 include Deps[creator: "aspects.screens.find_or_creator", view: "views.screens.sleep.new"]
  8. 1 def call device
  9. 4 creator.call model_id: device.model_id,
  10. content: String.new(view.call(device:)),
  11. **device.screen_attributes("sleep")
  12. end
  13. end
  14. end
  15. end
  16. end

app/aspects/screens/temp_pather.rb

100.0% lines covered

100.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "inspectable"
  4. 1 module Terminus
  5. 1 module Aspects
  6. 1 module Screens
  7. # Saves content as image to temporary file path for optional processing.
  8. 1 class TempPather
  9. 1 include Deps["aspects.sanitizer", "aspects.screens.shoter", "aspects.screens.converter"]
  10. 1 include Dry::Monads[:result]
  11. 1 include Inspectable[sanitizer: :type]
  12. 57 def call(mold, &) = Pathname.mktmpdir { process mold, it, & }
  13. 1 private
  14. 1 def process mold, directory
  15. 56 mold.output_path = directory.join mold.file_name
  16. 112 capture_input(mold, directory).bind { converter.call mold }
  17. 56 then: 54 else: 2 .bind { |path| block_given? ? yield(path) : path }
  18. end
  19. 1 def capture_input mold, directory
  20. 56 content = sanitizer.call mold.content
  21. 56 shoter.call(content, directory.join("input.png"), **mold.viewport)
  22. 56 .fmap { |path| mold.input_path = path }
  23. end
  24. end
  25. end
  26. end
  27. end

app/aspects/screens/upserter.rb

100.0% lines covered

100.0% branches covered

17 relevant lines. 17 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 module Terminus
  4. 1 module Aspects
  5. 1 module Screens
  6. # Creates or updates a screen.
  7. 1 class Upserter
  8. 1 include Deps[
  9. "aspects.screens.mold_builder",
  10. "aspects.screens.upserters.html",
  11. "aspects.screens.upserters.preprocessed",
  12. "aspects.screens.upserters.unprocessed"
  13. ]
  14. 1 include Dry::Monads[:result]
  15. 1 def call **parameters
  16. 32 in: 26 case parameters
  17. 26 in label:, name:, content: then handle_html label:, name:, content:, **parameters
  18. in: 2 in label:, name:, uri:, preprocessed: true
  19. 2 in: 2 handle_preprocessed label:, name:, content: uri, **parameters
  20. 2 else: 2 in label:, name:, uri: then handle_unprocessed label:, name:, content: uri, **parameters
  21. 2 else Failure "Invalid parameters: #{parameters.inspect}."
  22. end
  23. end
  24. 1 private
  25. 23 def handle_html(**) = mold_builder.call(**).bind { html.call it }
  26. 3 def handle_unprocessed(**) = mold_builder.call(**).bind { unprocessed.call it }
  27. 3 def handle_preprocessed(**) = mold_builder.call(**).bind { preprocessed.call it }
  28. end
  29. end
  30. end
  31. end

app/aspects/screens/upserters/html.rb

100.0% lines covered

100.0% branches covered

13 relevant lines. 13 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "initable"
  4. 1 module Terminus
  5. 1 module Aspects
  6. 1 module Screens
  7. 1 module Upserters
  8. # Creates screen record with image attachment from HTML content.
  9. 1 class HTML
  10. 1 include Deps["aspects.screens.temp_pather", repository: "repositories.screen"]
  11. 1 include Initable[struct: proc { Terminus::Structs::Screen.new }]
  12. 1 include Dry::Monads[:result]
  13. 1 def call mold
  14. 23 temp_pather.call mold do |path|
  15. 23 Success repository.upsert_with_image(path, mold, struct)
  16. end
  17. end
  18. end
  19. end
  20. end
  21. end
  22. end

app/aspects/screens/upserters/preprocessed.rb

100.0% lines covered

100.0% branches covered

18 relevant lines. 18 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 module Terminus
  4. 1 module Aspects
  5. 1 module Screens
  6. 1 module Upserters
  7. # Creates screen record with image attachment from preprocesed image URI.
  8. 1 class Preprocessed
  9. 1 include Deps["mini_magick.image", repository: "repositories.screen"]
  10. 1 include Dry::Monads[:result]
  11. 1 def initialize(struct: Terminus::Structs::Screen.new, **)
  12. 48 @struct = struct
  13. 48 super(**)
  14. end
  15. 4 def call(mold) = Pathname.mktmpdir { process mold, it }
  16. 1 private
  17. 1 attr_reader :struct
  18. 1 def process mold, directory
  19. 3 path = Pathname(directory).join "input.png"
  20. 6 image.open(mold.content).write(path).then { save mold, path }
  21. end
  22. 1 def save(mold, path) = Success repository.upsert_with_image(path, mold, struct)
  23. end
  24. end
  25. end
  26. end
  27. end

app/aspects/screens/upserters/unprocessed.rb

100.0% lines covered

100.0% branches covered

22 relevant lines. 22 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "refinements/struct"
  4. 1 module Terminus
  5. 1 module Aspects
  6. 1 module Screens
  7. 1 module Upserters
  8. # Creates screen record with image attachment from unprocessed image URI.
  9. 1 class Unprocessed
  10. 1 include Deps[
  11. "mini_magick.image",
  12. "aspects.screens.converter",
  13. repository: "repositories.screen"
  14. ]
  15. 1 include Dry::Monads[:result]
  16. 1 using Refinements::Struct
  17. 1 def initialize(struct: Terminus::Structs::Screen.new, **)
  18. 48 @struct = struct
  19. 48 super(**)
  20. end
  21. 4 def call(mold) = Pathname.mktmpdir { process mold, it }
  22. 1 private
  23. 1 attr_reader :struct
  24. 1 def process mold, directory
  25. 3 mold.with! input_path: Pathname(directory).join("input.png"),
  26. output_path: directory.join(mold.file_name)
  27. 3 image.open(mold.content)
  28. .write(mold.input_path)
  29. 3 .then { converter.call mold }
  30. 3 .bind { |path| save mold, path }
  31. end
  32. 1 def save(mold, path) = Success repository.upsert_with_image(path, mold, struct)
  33. end
  34. end
  35. end
  36. end
  37. end

app/aspects/screens/welcomer.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Aspects
  4. 1 module Screens
  5. # Creates welcome screen for new device.
  6. 1 class Welcomer
  7. 1 include Deps[creator: "aspects.screens.find_or_creator", view: "views.screens.welcome.new"]
  8. 1 def call device
  9. 26 creator.call model_id: device.model_id,
  10. content: String.new(view.call(device:)),
  11. **device.screen_attributes("welcome")
  12. end
  13. end
  14. end
  15. end
  16. end

app/aspects/users/creator.rb

100.0% lines covered

100.0% branches covered

25 relevant lines. 25 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Aspects
  4. 1 module Users
  5. # Validates and creates a new user complete with account and membership.
  6. 1 class Creator
  7. 1 include Deps[
  8. "aspects.password_encryptor",
  9. contract: "contracts.users.create",
  10. repository: "repositories.user",
  11. password_relation: "relations.user_password_hash",
  12. account_repository: "repositories.account",
  13. membership_relation: "relations.membership"
  14. ]
  15. 1 include Dry::Monads[:result]
  16. 1 DEFAULTS = {name: "default", label: "Default"}.freeze
  17. 1 def initialize(defaults: DEFAULTS, **)
  18. 9 @defaults = defaults
  19. 9 super(**)
  20. end
  21. 1 def call(**attributes)
  22. 9 result = contract.call(attributes).to_monad
  23. 9 then: 2 else: 7 return result if result.failure?
  24. 7 Success create_user(attributes[:user], attributes.fetch(:account, {}))
  25. end
  26. 1 private
  27. 1 attr_reader :defaults
  28. 1 def create_user user_attributes, account_attributes
  29. 7 account_attributes = defaults.merge account_attributes
  30. 7 password = user_attributes.delete :password
  31. 7 repository.create(**user_attributes).tap do |user|
  32. 7 password_relation.insert id: user.id, password_hash: password_encryptor.call(password)
  33. 7 create_membership user, account_attributes
  34. end
  35. end
  36. 1 def create_membership user, account_attributes
  37. 7 account_repository.find_or_create(**account_attributes).then do |account|
  38. 7 membership_relation.insert account_id: account.id, user_id: user.id
  39. end
  40. end
  41. end
  42. end
  43. end
  44. end

app/aspects/users/updater.rb

100.0% lines covered

100.0% branches covered

23 relevant lines. 23 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "refinements/string"
  3. 1 module Terminus
  4. 1 module Aspects
  5. 1 module Users
  6. # Validates and updates an existing user.
  7. 1 class Updater
  8. 1 include Deps[
  9. "aspects.password_encryptor",
  10. contract: "contracts.users.update",
  11. repository: "repositories.user",
  12. password_relation: "relations.user_password_hash"
  13. ]
  14. 1 include Dry::Monads[:result]
  15. 1 using Refinements::String
  16. 1 def call(**attributes)
  17. 7 result = contract.call(attributes).to_monad
  18. 7 then: 2 else: 5 return result if result.failure?
  19. 5 Success update(attributes[:id], attributes[:user])
  20. end
  21. 1 private
  22. 1 def update id, attributes
  23. 5 password = attributes.delete :password
  24. 10 user = repository.update(id, **attributes).then { repository.find id }
  25. 5 update_password user, password
  26. end
  27. 1 def update_password user, value
  28. 5 then: 4 else: 1 return user if String(value).blank?
  29. 1 id = user.id
  30. 1 password_relation.by_pk(id).delete
  31. 1 password_relation.upsert id: id, password_hash: password_encryptor.call(value)
  32. 1 user
  33. end
  34. end
  35. end
  36. end
  37. end

app/contract.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "refinements/pathname"
  3. 1 module Terminus
  4. # Defines user create contract.
  5. 1 class Contract < Dry::Validation::Contract
  6. 1 using Refinements::Pathname
  7. 1 config.messages.backend = :i18n
  8. 1 config.messages.load_paths.merge Hanami.app.root.join("config/locales").files("*.yml")
  9. end
  10. end

app/contracts/devices/create.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Contracts
  4. 1 module Devices
  5. # The contract for device creates.
  6. 1 class Create < Contract
  7. 2 params { required(:device).filled Schemas::Devices::Upsert }
  8. 1 rule device: :sleep_start_at, &Rules::SleepStartAt
  9. 1 rule device: :sleep_stop_at, &Rules::SleepStopAt
  10. end
  11. end
  12. end
  13. end

app/contracts/devices/patch.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Contracts
  4. 1 module Devices
  5. # The contract for device patches.
  6. 1 class Patch < Contract
  7. 1 params do
  8. 1 required(:id).filled :integer
  9. 1 required(:device).filled Schemas::Devices::Patch
  10. end
  11. 1 rule device: :sleep_start_at, &Rules::SleepStartAt
  12. 1 rule device: :sleep_stop_at, &Rules::SleepStopAt
  13. end
  14. end
  15. end
  16. end

app/contracts/devices/update.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Contracts
  4. 1 module Devices
  5. # The contract for device updates.
  6. 1 class Update < Contract
  7. 1 params do
  8. 1 required(:id).filled :integer
  9. 1 required(:device).filled Schemas::Devices::Upsert
  10. end
  11. 1 rule device: :sleep_start_at, &Rules::SleepStartAt
  12. 1 rule device: :sleep_stop_at, &Rules::SleepStopAt
  13. end
  14. end
  15. end
  16. end

app/contracts/extensions/create.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Contracts
  4. 1 module Extensions
  5. # The contract for extension creation.
  6. 1 class Create < Contract
  7. 1 config.messages.namespace = :extension
  8. 1 params do
  9. 1 required(:extension).filled Schemas::Extensions::Upsert
  10. 1 optional(:model_ids).filled :array
  11. end
  12. 1 rule extension: :interval, &Rules::Cron
  13. end
  14. end
  15. end
  16. end

app/contracts/extensions/exchanges/create.rb

100.0% lines covered

100.0% branches covered

8 relevant lines. 8 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Contracts
  4. 1 module Extensions
  5. 1 module Exchanges
  6. # The contract for extension exchange creation.
  7. 1 class Create < Contract
  8. 1 params do
  9. 1 required(:extension_id).filled :integer
  10. 1 required(:exchange).filled Schemas::Extensions::Exchanges::Upsert
  11. end
  12. end
  13. end
  14. end
  15. end
  16. end

app/contracts/extensions/exchanges/update.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Contracts
  4. 1 module Extensions
  5. 1 module Exchanges
  6. # The contract for extension exchange updating.
  7. 1 class Update < Contract
  8. 1 params do
  9. 1 required(:extension_id).filled :integer
  10. 1 required(:id).filled :integer
  11. 1 required(:exchange).filled Schemas::Extensions::Exchanges::Upsert
  12. end
  13. end
  14. end
  15. end
  16. end
  17. end

app/contracts/extensions/update.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Contracts
  4. 1 module Extensions
  5. # The contract for extension updates.
  6. 1 class Update < Contract
  7. 1 config.messages.namespace = :extension
  8. 1 params do
  9. 1 required(:id).filled :integer
  10. 1 required(:extension).filled Schemas::Extensions::Upsert
  11. 1 optional(:model_ids).filled :array
  12. end
  13. 1 rule extension: :interval, &Rules::Cron
  14. end
  15. end
  16. end
  17. end

app/contracts/models/clone.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Contracts
  4. 1 module Models
  5. # The contract for model cloning.
  6. 1 class Clone < Contract
  7. 1 config.messages.namespace = :model
  8. 1 params do
  9. 1 required(:model_id).filled :integer
  10. 1 required(:model).filled Schemas::Models::Upsert
  11. end
  12. 1 rule model: :mime_type, &Rules::ImageMimeType
  13. end
  14. end
  15. end
  16. end

app/contracts/models/create.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Contracts
  4. 1 module Models
  5. # The contract for model creation.
  6. 1 class Create < Contract
  7. 1 config.messages.namespace = :model
  8. 2 params { required(:model).filled Schemas::Models::Upsert }
  9. 1 rule model: :mime_type, &Rules::ImageMimeType
  10. end
  11. end
  12. end
  13. end

app/contracts/models/update.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Contracts
  4. 1 module Models
  5. # The contract for model updates.
  6. 1 class Update < Contract
  7. 1 config.messages.namespace = :model
  8. 1 params do
  9. 1 required(:id).filled :integer
  10. 1 required(:model).filled Schemas::Models::Upsert
  11. end
  12. 1 rule model: :mime_type, &Rules::ImageMimeType
  13. end
  14. end
  15. end
  16. end

app/contracts/rules/cron.rb

100.0% lines covered

100.0% branches covered

8 relevant lines. 8 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 module Terminus
  4. 1 module Contracts
  5. 1 module Rules
  6. 1 Cron = lambda do
  7. 26 attributes = values.fetch(:extension).slice :interval, :unit
  8. 26 case attributes
  9. in {unit: "none"} \
  10. | {unit: "minute", interval: 0..59} \
  11. | {unit: "hour", interval: 0..23} \
  12. | {unit: "day", interval: 1..31} \
  13. in: 14 | {unit: "week", interval: 0..6} \
  14. 14 | {unit: "month", interval: 1..12} then next
  15. else: 12 else
  16. 12 key.failure "invalid schedule for #{attributes[:unit]} #{attributes[:interval]}."
  17. end
  18. end
  19. end
  20. end
  21. end

app/contracts/rules/image_mime_type.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 module Terminus
  4. 1 module Contracts
  5. 1 module Rules
  6. 1 ImageMimeType = lambda do
  7. 18 then: 13 else: 5 next if values.dig(:model, :mime_type).start_with? "image/"
  8. 5 key.failure "must be an image"
  9. end
  10. end
  11. end
  12. end

app/contracts/rules/sleep_start_at.rb

100.0% lines covered

100.0% branches covered

8 relevant lines. 8 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 module Terminus
  4. 1 module Contracts
  5. 1 module Rules
  6. 1 SleepStartAt = lambda do
  7. 27 stop_at = values.dig :device, :sleep_stop_at
  8. 27 then: 4 else: 23 if value && stop_at.nil? then key.failure "must have corresponding stop time"
  9. 23 then: 5 else: 18 elsif value.nil? && stop_at then key.failure "must be filled"
  10. 18 else next
  11. end
  12. end
  13. end
  14. end
  15. end

app/contracts/rules/sleep_stop_at.rb

100.0% lines covered

100.0% branches covered

8 relevant lines. 8 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 module Terminus
  4. 1 module Contracts
  5. 1 module Rules
  6. 1 SleepStopAt = lambda do
  7. 27 start_at = values.dig :device, :sleep_start_at
  8. 27 then: 5 else: 22 if value && start_at.nil? then key.failure "must have corresponding start time"
  9. 22 then: 4 else: 18 elsif value.nil? && start_at then key.failure "must be filled"
  10. 18 else next
  11. end
  12. end
  13. end
  14. end
  15. end

app/contracts/users/create.rb

100.0% lines covered

100.0% branches covered

13 relevant lines. 13 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Contracts
  4. 1 module Users
  5. # Defines user create contract.
  6. 1 class Create < Contract
  7. 1 params do
  8. 1 required(:user).filled(:hash) do
  9. 1 required(:name).filled :string
  10. 1 required(:email).filled :string
  11. 1 optional(:password).maybe(:string, min_size?: 10)
  12. 1 optional(:status_id).filled :integer
  13. end
  14. 1 optional(:account).filled(:hash) do
  15. 1 required(:name).filled :string
  16. 1 required(:label).filled :string
  17. end
  18. end
  19. end
  20. end
  21. end
  22. end

app/contracts/users/update.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Contracts
  4. 1 module Users
  5. # Defines user update contract.
  6. 1 class Update < Contract
  7. 1 params do
  8. 1 required(:id).filled :integer
  9. 1 required(:user).filled(:hash) do
  10. 1 required(:name).filled :string
  11. 1 required(:email).filled :string
  12. 1 optional(:password).maybe(:string, min_size?: 10)
  13. 1 required(:status_id).filled :integer
  14. end
  15. end
  16. end
  17. end
  18. end
  19. end

app/db/relation.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "hanami/db/relation"
  3. 1 module Terminus
  4. 1 module DB
  5. # The application database base relation.
  6. 1 class Relation < Hanami::DB::Relation
  7. end
  8. end
  9. end

app/db/repository.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "hanami/db/repo"
  3. 1 module Terminus
  4. 1 module DB
  5. # The application database base repository.
  6. 1 class Repository < Hanami::DB::Repo
  7. end
  8. end
  9. end

app/db/struct.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "hanami/db/struct"
  3. 1 module Terminus
  4. 1 module DB
  5. # The application database base struct.
  6. 1 class Struct < Hanami::DB::Struct
  7. end
  8. end
  9. end

app/jobs/base.rb

100.0% lines covered

100.0% branches covered

8 relevant lines. 8 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 require "dry/monads"
  4. 1 require "sidekiq"
  5. 1 module Terminus
  6. 1 module Jobs
  7. # The base abstract class for which all jobs inherit from.
  8. 1 class Base
  9. 1 include Dry::Monads[:result]
  10. 1 include Sidekiq::Job
  11. 1 sidekiq_options queue: "within_1_hour"
  12. end
  13. end
  14. end

app/jobs/batches/extension.rb

100.0% lines covered

100.0% branches covered

18 relevant lines. 18 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 require "initable"
  4. 1 module Terminus
  5. 1 module Jobs
  6. 1 module Batches
  7. # Enqueues a job for each model ID.
  8. 1 class Extension < Base
  9. 1 include Deps[repository: "repositories.extension"]
  10. 1 include Initable[job: Jobs::Extensions::Screen]
  11. 1 sidekiq_options queue: "within_1_minute"
  12. 1 def perform id
  13. 6 extension = repository.find id
  14. 6 else: 4 then: 2 return Failure "Unable to enqueue jobs for extension: #{id}." unless extension
  15. 4 then: 1 else: 3 extension.devices.any? ? enqueue_devices(extension) : enqueue_models(extension)
  16. 4 Success "Enqueued jobs for extension: #{id}."
  17. end
  18. 1 private
  19. 1 def enqueue_models extension
  20. 4 extension.models.each { |model| job.perform_async extension.id, model.id }
  21. end
  22. 1 def enqueue_devices extension
  23. 2 extension.devices.each { |device| job.perform_async extension.id, nil, device.id }
  24. end
  25. end
  26. end
  27. end
  28. end

app/jobs/extensions/exchange_refresh.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 module Terminus
  4. 1 module Jobs
  5. 1 module Extensions
  6. # Refreshes exchange with new responses.
  7. 1 class ExchangeRefresh < Base
  8. 1 include Deps[
  9. "aspects.extensions.exchanges.refresher",
  10. repository: "repositories.extension_exchange"
  11. ]
  12. 1 sidekiq_options queue: "within_1_minute"
  13. 1 def perform id
  14. 3 exchange = repository.find id
  15. 3 else: 2 then: 1 return Failure "Unable to find exchange ID: #{id}." unless exchange
  16. 2 refresher.call exchange
  17. end
  18. end
  19. end
  20. end
  21. end

app/jobs/extensions/screen.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 module Terminus
  4. 1 module Jobs
  5. 1 module Extensions
  6. # Creates screen for extension and model or device ID.
  7. 1 class Screen < Base
  8. 1 include Deps["aspects.extensions.screen_upserter", repository: "repositories.extension"]
  9. 1 sidekiq_options queue: "within_1_minute"
  10. 1 def perform id, model_id = nil, device_id = nil
  11. 5 extension = repository.find id
  12. 5 else: 4 then: 1 return Failure "Unable to find by extension ID: #{id}." unless extension
  13. 4 screen_upserter.call extension, model_id:, device_id:
  14. end
  15. end
  16. end
  17. end
  18. end

app/jobs/synchronizers/firmware.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 module Terminus
  4. 1 module Jobs
  5. 1 module Synchronizers
  6. # Synchronizes TRMNL Firmware for local use.
  7. 1 class Firmware < Base
  8. 1 include Deps[:settings, :logger, "aspects.firmware.synchronizer"]
  9. 1 sidekiq_options queue: "within_1_minute"
  10. 1 def perform
  11. 3 then: 1 else: 2 return synchronizer.call if settings.firmware_synchronizer
  12. 4 logger.info { "Firmware synchronization is disabled." }
  13. end
  14. end
  15. end
  16. end
  17. end

app/jobs/synchronizers/font.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 module Terminus
  4. 1 module Jobs
  5. 1 module Synchronizers
  6. # Synchronizes TRMNL Framework fonts for local use.
  7. 1 class Font < Base
  8. 1 include Deps["aspects.fonts.synchronizer"]
  9. 1 sidekiq_options queue: "within_1_minute"
  10. 1 def perform = synchronizer.call
  11. end
  12. end
  13. end
  14. end

app/jobs/synchronizers/model.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 module Terminus
  4. 1 module Jobs
  5. 1 module Synchronizers
  6. # Synchronizes TRMNL models for local use.
  7. 1 class Model < Base
  8. 1 include Deps[
  9. :settings,
  10. :logger,
  11. palette: "aspects.palettes.synchronizer",
  12. model: "aspects.models.synchronizer"
  13. ]
  14. 1 sidekiq_options queue: "within_1_minute"
  15. 1 def perform
  16. 7 then: 5 if settings.model_synchronizer
  17. 7 palette.call.bind { model.call }
  18. else: 2 else
  19. 4 logger.info { "Model synchronization is disabled." }
  20. end
  21. end
  22. end
  23. end
  24. end
  25. end

app/jobs/synchronizers/sensor.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 module Terminus
  4. 1 module Jobs
  5. 1 module Synchronizers
  6. # Synchronizes server hosted sensor data.
  7. 1 class Sensor < Base
  8. 1 include Deps["aspects.devices.sensors.synchronizer"]
  9. 1 sidekiq_options queue: "within_1_minute"
  10. 1 def perform = synchronizer.call
  11. end
  12. end
  13. end
  14. end

app/providers/logger.rb

100.0% lines covered

100.0% branches covered

25 relevant lines. 25 lines covered and 0 lines missed.
3 total branches, 3 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 module Terminus
  4. 1 module Providers
  5. # The logger provider.
  6. 1 class Logger < Hanami::Provider::Source
  7. 3 RESOLVER = proc { Object.const_get "Cogger" }
  8. 1 def initialize(environment: Hanami.env, resolver: RESOLVER, **)
  9. 9 @environment = environment
  10. 9 @resolver = resolver
  11. 9 @id = Hanami.app.namespace.to_s.downcase.to_sym
  12. 9 super(**)
  13. end
  14. 1 def prepare = require "cogger"
  15. 1 def start
  16. 8 add_filters
  17. 8 register :logger, build_instance
  18. end
  19. 1 private
  20. 1 attr_reader :environment, :resolver, :id
  21. 1 def add_filters
  22. 8 cogger.add_filters :api_key,
  23. :csrf,
  24. :HTTP_ACCESS_TOKEN,
  25. :HTTP_ID,
  26. :mac_address,
  27. :password,
  28. :password_confirmation
  29. end
  30. 1 def build_instance
  31. 8 io = "log/#{environment}.log"
  32. 8 case environment
  33. when: 5 when :test
  34. 5 when: 2 cogger.new(id:, io: StringIO.new, formatter: :json, level: :debug).add_stream io:
  35. 2 else: 1 when :development then cogger.new(id:).add_stream(io:, formatter: :json)
  36. 1 else cogger.new id:, formatter: :json
  37. end
  38. end
  39. 1 def cogger
  40. 16 @cogger ||= resolver.call
  41. end
  42. end
  43. end
  44. end

app/providers/sidekiq.rb

100.0% lines covered

100.0% branches covered

26 relevant lines. 26 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 module Terminus
  4. 1 module Providers
  5. # The Sidekiq provider.
  6. 1 class Sidekiq < Hanami::Provider::Source
  7. 1 include Deps[:logger]
  8. 2 RESOLVER = proc { Object.const_get "Sidekiq" }
  9. 1 def initialize(resolver: RESOLVER, **)
  10. 4 @resolver = resolver
  11. 4 super(**)
  12. end
  13. 1 def prepare
  14. 2 require "sidekiq"
  15. 2 require "sidekiq-scheduler"
  16. 2 require "yaml"
  17. end
  18. 1 def start
  19. 3 configure_server
  20. 3 configure_client
  21. 3 register :sidekiq, sidekiq
  22. end
  23. 1 private
  24. 1 attr_reader :resolver
  25. 1 def configure_client
  26. 3 sidekiq.configure_client do |configuration|
  27. 1 configuration.redis = {url: slice[:settings].keyvalue_url}
  28. 1 configuration.logger = slice[:logger]
  29. end
  30. end
  31. 1 def configure_server
  32. skipped # :nocov:
  33. skipped sidekiq.configure_server do |configuration|
  34. skipped configuration.redis = {url: slice[:settings].keyvalue_url}
  35. skipped configuration.logger = slice[:logger]
  36. skipped configuration.on(:startup) { load_schedule }
  37. skipped end
  38. skipped # :nocov:
  39. end
  40. 1 def sidekiq
  41. 9 @sidekiq ||= resolver.call
  42. end
  43. 1 def load_schedule
  44. skipped # :nocov:
  45. skipped jobs = YAML.load_file slice.root.join("config/sidekiq_scheduler.yml")
  46. skipped
  47. skipped jobs.each do |schedule_name, options|
  48. skipped resolver.call.set_schedule schedule_name, options
  49. skipped job_name = options["class"]
  50. skipped Object.const_get(job_name).perform_in 0
  51. skipped rescue NameError, TypeError
  52. skipped logger.error { "Unable to initialize job: #{job_name}." }
  53. skipped end
  54. skipped # :nocov:
  55. end
  56. end
  57. end
  58. end

app/relations/account.rb

100.0% lines covered

100.0% branches covered

5 relevant lines. 5 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Relations
  4. # The account relation.
  5. 1 class Account < DB::Relation
  6. 1 schema :account, infer: true do
  7. 2 associations { has_many :memberships, relation: :membership }
  8. end
  9. end
  10. end
  11. end

app/relations/device.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Relations
  4. # The device relation.
  5. 1 class Device < DB::Relation
  6. 1 schema :device, infer: true do
  7. 1 associations do
  8. 1 belongs_to :model, relation: :model
  9. 1 belongs_to :playlist, relation: :playlist
  10. 1 has_many :device_logs, relation: :device_log, as: :logs
  11. 1 has_many :device_sensors, relation: :device_sensor, as: :sensors
  12. end
  13. end
  14. end
  15. end
  16. end

app/relations/device_log.rb

100.0% lines covered

100.0% branches covered

5 relevant lines. 5 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Relations
  4. # The device log relation.
  5. 1 class DeviceLog < DB::Relation
  6. 1 schema :device_log, infer: true do
  7. 2 associations { belongs_to :device, relation: :device }
  8. end
  9. end
  10. end
  11. end

app/relations/device_sensor.rb

100.0% lines covered

100.0% branches covered

5 relevant lines. 5 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Relations
  4. # The device sensor relation.
  5. 1 class DeviceSensor < DB::Relation
  6. 1 schema :device_sensor, infer: true do
  7. 2 associations { belongs_to :device, relation: :device }
  8. end
  9. end
  10. end
  11. end

app/relations/extension.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Relations
  4. # The extension relation.
  5. 1 class Extension < DB::Relation
  6. 1 schema :extension, infer: true do
  7. 1 associations do
  8. 1 has_many :extension_devices, relation: :extension_device
  9. 1 has_many :devices, through: :extension_device, relation: :device, as: :devices
  10. 1 has_many :extension_models, relation: :extension_model
  11. 1 has_many :models, through: :extension_model, relation: :model, as: :models
  12. 1 has_many :extension_exchanges, relation: :extension_exchange
  13. end
  14. end
  15. end
  16. end
  17. end

app/relations/extension_device.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Relations
  4. # The extension and device join relation.
  5. 1 class ExtensionDevice < DB::Relation
  6. 1 schema :extension_device, infer: true do
  7. 1 associations do
  8. 1 belongs_to :extension, relation: :extension
  9. 1 belongs_to :device, relation: :device
  10. end
  11. end
  12. end
  13. end
  14. end

app/relations/extension_exchange.rb

100.0% lines covered

100.0% branches covered

5 relevant lines. 5 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Relations
  4. # The extension exchange relation.
  5. 1 class ExtensionExchange < DB::Relation
  6. 1 schema :extension_exchange, infer: true do
  7. 2 associations { belongs_to :extension, relation: :extension }
  8. end
  9. end
  10. end
  11. end

app/relations/extension_model.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Relations
  4. # The extension and model join relation.
  5. 1 class ExtensionModel < DB::Relation
  6. 1 schema :extension_model, infer: true do
  7. 1 associations do
  8. 1 belongs_to :extension, relation: :extension
  9. 1 belongs_to :model, relation: :model
  10. end
  11. end
  12. end
  13. end
  14. end

app/relations/firmware.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Relations
  4. # The firmware relation.
  5. 1 class Firmware < DB::Relation
  6. 1 schema :firmware, infer: true
  7. 1 def by_version_desc
  8. 80 order Sequel.desc(Sequel.function(:string_to_array, :version, ".").cast("int[]"))
  9. end
  10. end
  11. end
  12. end

app/relations/membership.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Relations
  4. # The membership relation.
  5. 1 class Membership < DB::Relation
  6. 1 schema :membership, infer: true do
  7. 1 associations do
  8. 1 belongs_to :account, relation: :account
  9. 1 belongs_to :user, relation: :user
  10. end
  11. end
  12. end
  13. end
  14. end

app/relations/model.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Relations
  4. # The model relation.
  5. 1 class Model < DB::Relation
  6. 1 schema :model, infer: true do
  7. 1 associations do
  8. 1 belongs_to :default_palette, relation: :palette
  9. 1 has_many :devices, relation: :device
  10. 1 has_many :screens, relation: :screen
  11. 1 has_many :extension_models, relation: :extension_model
  12. 1 has_many :extensions, through: :extension_model, relation: :extension
  13. 1 has_many :model_palettes, relation: :model_palette
  14. 1 has_many :palettes, through: :model_palette, relation: :palette
  15. end
  16. end
  17. end
  18. end
  19. end

app/relations/model_palette.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Relations
  4. # The model and palette join relation.
  5. 1 class ModelPalette < DB::Relation
  6. 1 schema :model_palette, infer: true do
  7. 1 associations do
  8. 1 belongs_to :model, relation: :model
  9. 1 belongs_to :palette, relation: :palette
  10. end
  11. end
  12. end
  13. end
  14. end

app/relations/palette.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Relations
  4. # The palette relation.
  5. 1 class Palette < DB::Relation
  6. 1 schema :palette, infer: true do
  7. 1 associations do
  8. 1 has_many :model_palettes, relation: :model_palette
  9. 1 has_many :models, through: :model_palette, relation: :model
  10. end
  11. end
  12. end
  13. end
  14. end

app/relations/playlist.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Relations
  4. # The playlist relation.
  5. 1 class Playlist < DB::Relation
  6. 1 schema :playlist, infer: true do
  7. 1 associations do
  8. 1 belongs_to :current_item, relation: :playlist_item
  9. 1 has_many :devices, relation: :device
  10. 1 has_many :playlist_items, relation: :playlist_item, as: :playlist_items, view: :ordered
  11. 1 has_many :screens,
  12. through: :playlist_item,
  13. relation: :screen,
  14. as: :screens,
  15. view: :ordered
  16. end
  17. end
  18. end
  19. end
  20. end

app/relations/playlist_item.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Relations
  4. # The playlist item relation.
  5. 1 class PlaylistItem < DB::Relation
  6. 1 schema :playlist_item, infer: true do
  7. 1 associations do
  8. 1 belongs_to :playlist, relation: :playlist
  9. 1 belongs_to :screen, relation: :screen
  10. end
  11. end
  12. 1 def ordered = select_append(:position).order :position
  13. 1 def next_item playlist_id:, after:
  14. 14 scope = combine(:screen).where(playlist_id:).order :position
  15. 28 next_or_previous = scope.where { position > after }
  16. .first
  17. 14 next_or_previous || scope.first
  18. end
  19. end
  20. end
  21. end

app/relations/screen.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Relations
  4. # The screen relation.
  5. 1 class Screen < DB::Relation
  6. 1 schema :screen, infer: true do
  7. 1 associations do
  8. 1 belongs_to :model, relation: :model
  9. 1 has_many :playlist_items, relation: :playlist_item, as: :playlist_items, view: :ordered
  10. 1 has_many :playlists, through: :playlist_item, relation: :playlist, as: :playlists
  11. end
  12. end
  13. 1 def ordered = select_append(playlist_item[:position]).order :position
  14. end
  15. end
  16. end

app/relations/user.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Relations
  4. # The user relation.
  5. 1 class User < DB::Relation
  6. 1 schema :user, infer: true do
  7. 1 associations do
  8. 1 belongs_to :user_status, relation: :user_status, as: :status
  9. 1 has_many :memberships, relation: :membership
  10. end
  11. end
  12. end
  13. end
  14. end

app/relations/user_password_hash.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Relations
  4. # The user password hash relation.
  5. 1 class UserPasswordHash < DB::Relation
  6. 1 schema :user_password_hash, infer: true
  7. end
  8. end
  9. end

app/relations/user_status.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Relations
  4. # The user status relation.
  5. 1 class UserStatus < DB::Relation
  6. 1 schema :user_status, infer: true
  7. end
  8. end
  9. end

app/repositories/account.rb

100.0% lines covered

100.0% branches covered

16 relevant lines. 16 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Repositories
  4. # The account repository.
  5. 1 class Account < DB::Repository[:account]
  6. 1 commands :create, delete: :by_pk
  7. 1 commands update: :by_pk,
  8. use: :timestamps,
  9. plugins_options: {timestamps: {timestamps: :updated_at}}
  10. 1 def all
  11. 4 account.order { created_at.asc }
  12. .to_a
  13. end
  14. 4 then: 2 else: 1 def find(id) = (account.by_pk(id).one if id)
  15. 1 def find_by(**) = account.where(**).one
  16. 1 def find_or_create(**) = find_by(**) || create(**)
  17. 1 def search key, value
  18. 3 account.where(Sequel.ilike(key, "%#{value}%"))
  19. 3 .order { created_at.asc }
  20. .to_a
  21. end
  22. 1 def where(**)
  23. 4 account.where(**)
  24. 4 .order { created_at.asc }
  25. .to_a
  26. end
  27. end
  28. end
  29. end

app/repositories/device.rb

100.0% lines covered

100.0% branches covered

24 relevant lines. 24 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Repositories
  4. # The device repository.
  5. 1 class Device < DB::Repository[:device]
  6. 1 commands :create, delete: :by_pk
  7. 1 commands update: :by_pk,
  8. use: :timestamps,
  9. plugins_options: {timestamps: {timestamps: :updated_at}}
  10. 1 def all
  11. 58 with_associations.order { created_at.asc }
  12. .to_a
  13. end
  14. 33 then: 31 else: 1 def find(id) = (with_associations.by_pk(id).one if id)
  15. 1 def find_by(**) = with_associations.where(**).one
  16. 1 def mirror_playlist ids, playlist_id
  17. 7 device.update playlist_id: Sequel.case({{id: ids} => playlist_id}, nil)
  18. end
  19. 1 def search key, value
  20. 7 device.combine(:model)
  21. .where(Sequel.ilike(key, "%#{value}%"))
  22. 7 .order { created_at.asc }
  23. .to_a
  24. end
  25. 1 def update_by_mac_address(value, **attributes)
  26. 13 device = find_by mac_address: value
  27. 13 then: 2 else: 11 return device if attributes.empty?
  28. 11 else: 8 then: 3 return unless device
  29. 8 update device.id, **attributes
  30. end
  31. 1 def where(**)
  32. 3 with_associations.where(**)
  33. 3 .order { created_at.asc }
  34. .to_a
  35. end
  36. 1 private
  37. 1 def with_associations = device.combine :model, :playlist
  38. end
  39. end
  40. end

app/repositories/device_log.rb

100.0% lines covered

100.0% branches covered

17 relevant lines. 17 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Repositories
  4. # The device log repository.
  5. 1 class DeviceLog < DB::Repository[:device_log]
  6. 1 commands :create, delete: :by_pk
  7. 1 commands update: :by_pk,
  8. use: :timestamps,
  9. plugins_options: {timestamps: {timestamps: :updated_at}}
  10. 1 def all
  11. 5 device_log.combine(:device)
  12. 5 .order { created_at.desc }
  13. .to_a
  14. end
  15. 8 then: 6 else: 1 def find(id) = (device_log.combine(:device).by_pk(id).one if id)
  16. 1 def delete_by_device(device_id, id) = device_log.where(device_id:, id:).delete
  17. 1 def delete_all_by_device(device_id) = device_log.where(device_id:).command(:delete).call
  18. 1 def search(key, value, **)
  19. 6 device_log.where(**)
  20. .where(Sequel.ilike(key, "%#{value}%"))
  21. 6 .order { created_at.asc }
  22. .to_a
  23. end
  24. 1 def where(**)
  25. 8 device_log.where(**)
  26. 8 .order { created_at.desc }
  27. .to_a
  28. end
  29. end
  30. end
  31. end

app/repositories/device_sensor.rb

100.0% lines covered

100.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Repositories
  4. # The device sensor repository.
  5. 1 class DeviceSensor < DB::Repository[:device_sensor]
  6. 1 commands :create, delete: :by_pk
  7. 1 def all
  8. 22 with_associations.order { created_at.desc }
  9. .to_a
  10. end
  11. 4 then: 2 else: 1 def find(id) = (with_associations.by_pk(id).one if id)
  12. 1 def find_by(**) = with_associations.where(**).one
  13. 1 def limited_where(max = 25, **)
  14. 12 device_sensor.where(**)
  15. .limit(max)
  16. 12 .order { created_at.desc }
  17. .to_a
  18. end
  19. 1 def search(key, value, **)
  20. 4 with_associations.where(**)
  21. .where(Sequel.ilike(key, "%#{value}%"))
  22. 4 .order { created_at.asc }
  23. .to_a
  24. end
  25. 1 def where(**)
  26. 44 with_associations.where(**)
  27. 44 .order { created_at.desc }
  28. .to_a
  29. end
  30. 1 private
  31. 1 def with_associations = device_sensor.combine :device
  32. end
  33. end
  34. end

app/repositories/extension.rb

100.0% lines covered

100.0% branches covered

47 relevant lines. 47 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Repositories
  4. # The extension repository.
  5. 1 class Extension < DB::Repository[:extension]
  6. 1 commands :create, delete: :by_pk
  7. 1 commands update: :by_pk,
  8. use: :timestamps,
  9. plugins_options: {timestamps: {timestamps: :updated_at}}
  10. 1 def all
  11. 26 extension.order { created_at.asc }
  12. .to_a
  13. end
  14. 1 def create_with_devices attributes, device_ids
  15. 4 transaction do
  16. 4 record = create attributes
  17. 4 create_associations :extension_device, record, :device_id, device_ids
  18. 2 record
  19. end
  20. end
  21. 1 def create_with_models attributes, model_ids
  22. 25 transaction do
  23. 25 record = create attributes
  24. 21 create_associations :extension_model, record, :model_id, model_ids
  25. 19 record
  26. end
  27. end
  28. 88 then: 83 else: 4 def find(id) = (with_associations.by_pk(id).one if id)
  29. 1 def find_by(**) = with_associations.where(**).one
  30. 1 def search key, value
  31. 7 extension.where(Sequel.ilike(key, "%#{value}%"))
  32. 7 .order { created_at.asc }
  33. .to_a
  34. end
  35. 1 def update_with_devices id, attributes, device_ids
  36. 19 transaction do
  37. 19 record = update id, attributes
  38. 19 update_associations :extension_device, id, :device_id, device_ids
  39. 19 record
  40. end
  41. end
  42. 1 def update_with_models id, attributes, model_ids
  43. 7 transaction do
  44. 7 record = update id, attributes
  45. 7 update_associations :extension_model, id, :model_id, model_ids
  46. 7 record
  47. end
  48. end
  49. 1 def where(**)
  50. 4 extension.where(**)
  51. 4 .order { created_at.asc }
  52. .to_a
  53. end
  54. 1 private
  55. 1 def with_associations = extension.combine :devices, :models
  56. # rubocop:todo Metrics/ParameterLists
  57. 1 def create_associations name, record, foreign_key, values
  58. 36 associations = values.map { |id| {extension_id: record.id, foreign_key => id} }
  59. 25 __send__(name).changeset(:create, associations).commit
  60. end
  61. # rubocop:enable Metrics/ParameterLists
  62. # :reek:FeatureEnvy
  63. # :reek:TooManyStatements
  64. # rubocop:todo Metrics/ParameterLists
  65. 1 def update_associations name, id, foreign_key, values
  66. 26 association = __send__ name
  67. 26 association.where(extension_id: id).exclude(foreign_key => values).delete
  68. 26 old_ids = association.where(extension_id: id, foreign_key => values).map(foreign_key)
  69. 40 new_ids = values.reject { |id| old_ids.include? id.to_i }
  70. 40 associations = new_ids.map { |model_id| {extension_id: id, foreign_key => model_id} }
  71. 26 association.changeset(:create, associations).commit
  72. end
  73. # rubocop:enable Metrics/ParameterLists
  74. end
  75. end
  76. end

app/repositories/extension_device.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Repositories
  4. # The extension device repository.
  5. 1 class ExtensionDevice < DB::Repository[:extension_device]
  6. 1 commands :create, delete: :by_pk
  7. 1 commands update: :by_pk,
  8. use: :timestamps,
  9. plugins_options: {timestamps: {timestamps: :updated_at}}
  10. 1 def all
  11. 12 extension_device.order { created_at.asc }
  12. .to_a
  13. end
  14. 4 then: 2 else: 1 def find(id) = (extension_device.by_pk(id).one if id)
  15. 1 def find_by(**) = extension_device.where(**).one
  16. 1 def where(**)
  17. 5 extension_device.where(**)
  18. 5 .order { created_at.asc }
  19. .to_a
  20. end
  21. end
  22. end
  23. end

app/repositories/extension_exchange.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Repositories
  4. # The extension exchange repository.
  5. 1 class ExtensionExchange < DB::Repository[:extension_exchange]
  6. 1 commands :create, delete: :by_pk
  7. 1 commands update: :by_pk,
  8. use: :timestamps,
  9. plugins_options: {timestamps: {timestamps: :updated_at}}
  10. 1 def all
  11. 8 extension_exchange.order { created_at.asc }
  12. .to_a
  13. end
  14. 13 then: 11 else: 1 def find(id) = (extension_exchange.by_pk(id).one if id)
  15. 1 def find_by(**) = extension_exchange.where(**).one
  16. 1 def where(**)
  17. 63 extension_exchange.where(**)
  18. 63 .order { created_at.asc }
  19. .to_a
  20. end
  21. end
  22. end
  23. end

app/repositories/extension_model.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Repositories
  4. # The extension model repository.
  5. 1 class ExtensionModel < DB::Repository[:extension_model]
  6. 1 commands :create, delete: :by_pk
  7. 1 commands update: :by_pk,
  8. use: :timestamps,
  9. plugins_options: {timestamps: {timestamps: :updated_at}}
  10. 1 def all
  11. 12 extension_model.order { created_at.asc }
  12. .to_a
  13. end
  14. 4 then: 2 else: 1 def find(id) = (extension_model.by_pk(id).one if id)
  15. 1 def find_by(**) = extension_model.where(**).one
  16. 1 def where(**)
  17. 5 extension_model.where(**)
  18. 5 .order { created_at.asc }
  19. .to_a
  20. end
  21. end
  22. end
  23. end

app/repositories/firmware.rb

100.0% lines covered

100.0% branches covered

21 relevant lines. 21 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Repositories
  4. # The firmware repository.
  5. 1 class Firmware < DB::Repository[:firmware]
  6. 1 include Deps[:shrine]
  7. 1 commands :create
  8. 1 commands update: :by_pk,
  9. use: :timestamps,
  10. plugins_options: {timestamps: {timestamps: :updated_at}}
  11. 1 def all = firmware.by_version_desc.to_a
  12. 1 def delete id
  13. 16 then: 6 else: 2 find(id).then { it.attachment_destroy if it }
  14. 8 firmware.by_pk(id).delete
  15. end
  16. 1 def delete_all
  17. 8 firmware.where { attachment_data.has_key "id" }
  18. 4 .select { attachment_data.get_text("id").as(:attachment_id) }
  19. .map(:attachment_id)
  20. 1 .each { shrine.storages[:store].delete it }
  21. 4 firmware.delete
  22. end
  23. 28 then: 26 else: 1 def find(id) = (firmware.by_pk(id).one if id)
  24. 1 def find_by(**) = firmware.where(**).one
  25. 1 def latest = all.first
  26. 1 def search key, value
  27. 8 firmware.where(Sequel.like(key, "%#{value}%"))
  28. 8 .order { created_at.asc }
  29. .to_a
  30. end
  31. end
  32. end
  33. end

app/repositories/model.rb

100.0% lines covered

100.0% branches covered

21 relevant lines. 21 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Repositories
  4. # The model repository.
  5. 1 class Model < DB::Repository[:model]
  6. 1 commands :create, delete: :by_pk
  7. 1 commands update: :by_pk,
  8. use: :timestamps,
  9. plugins_options: {timestamps: {timestamps: :updated_at}}
  10. 1 def all
  11. 122 with_associations.order { label.asc }
  12. .to_a
  13. end
  14. 1 def delete_all(**) = model.where(**).delete
  15. 132 then: 105 else: 26 def find(id) = (with_associations.by_pk(id).one if id)
  16. 1 def find_by(**) = with_associations.where(**).one
  17. 1 def find_or_create(key, value, **)
  18. 2 with_associations.where(key => value)
  19. .one
  20. 2 .then { |record| record || create(name: value, **) }
  21. end
  22. 1 def search key, value
  23. 7 with_associations.where(Sequel.ilike(key, "%#{value}%"))
  24. 7 .order { created_at.asc }
  25. .to_a
  26. end
  27. 1 def where(**)
  28. 18 with_associations.where(**)
  29. 18 .order { created_at.asc }
  30. .to_a
  31. end
  32. 1 private
  33. 1 def with_associations = model.combine(:default_palette)
  34. end
  35. end
  36. end

app/repositories/model_palette.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Repositories
  4. # The model palette repository.
  5. 1 class ModelPalette < DB::Repository[:model_palette]
  6. 1 commands :create, delete: :by_pk
  7. 1 commands update: :by_pk,
  8. use: :timestamps,
  9. plugins_options: {timestamps: {timestamps: :updated_at}}
  10. 1 def all
  11. 4 model_palette.order { created_at.asc }
  12. .to_a
  13. end
  14. 4 then: 2 else: 1 def find(id) = (model_palette.by_pk(id).one if id)
  15. 1 def find_by(**) = model_palette.where(**).one
  16. 1 def where(**)
  17. 31 model_palette.where(**)
  18. 31 .order { created_at.asc }
  19. .to_a
  20. end
  21. end
  22. end
  23. end

app/repositories/palette.rb

100.0% lines covered

100.0% branches covered

16 relevant lines. 16 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Repositories
  4. # The palette repository.
  5. 1 class Palette < DB::Repository[:palette]
  6. 1 commands :create, delete: :by_pk
  7. 1 commands update: :by_pk,
  8. use: :timestamps,
  9. plugins_options: {timestamps: {timestamps: :updated_at}}
  10. 1 def all
  11. 60 palette.order { label.asc }
  12. .to_a
  13. end
  14. 1 def delete_all(**) = palette.where(**).delete
  15. 65 then: 3 else: 61 def find(id) = (palette.by_pk(id).one if id)
  16. 1 def find_by(**) = palette.where(**).one
  17. 1 def search key, value
  18. 3 palette.where(Sequel.ilike(key, "%#{value}%"))
  19. 3 .order { label.asc }
  20. .to_a
  21. end
  22. 1 def where(**)
  23. 22 palette.where(**)
  24. 22 .order { label.asc }
  25. .to_a
  26. end
  27. end
  28. end
  29. end

app/repositories/playlist.rb

100.0% lines covered

100.0% branches covered

38 relevant lines. 38 lines covered and 0 lines missed.
10 total branches, 10 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Repositories
  4. # The playlist repository.
  5. 1 class Playlist < DB::Repository[:playlist]
  6. 1 commands :create, delete: :by_pk
  7. 1 commands update: :by_pk,
  8. use: :timestamps,
  9. plugins_options: {timestamps: {timestamps: :updated_at}}
  10. 1 def all
  11. 44 with_current_item.order { created_at.asc }
  12. .to_a
  13. end
  14. 1 def create_with_items attributes, collection
  15. 11 transaction do
  16. 11 record = create attributes
  17. 11 items = create_items record, collection
  18. 9 then: 8 else: 1 collection.any? ? update(record.id, current_item_id: items.first.id) : record
  19. end
  20. end
  21. 91 then: 71 else: 19 def find(id) = (with_current_item.by_pk(id).one if id)
  22. 1 def find_by(**) = with_current_item.where(**).one
  23. 1 def search key, value
  24. 7 playlist.where(Sequel.ilike(key, "%#{value}%"))
  25. 7 .order { created_at.asc }
  26. .to_a
  27. end
  28. 1 def update_current_item record, item
  29. 13 record_id = record.id
  30. 13 then: 11 else: 2 update record_id, current_item_id: item.id if item
  31. 13 find record_id
  32. end
  33. 1 def update_with_items id, attributes, collection
  34. 8 transaction do
  35. 8 record = update id, attributes
  36. 8 then: 6 else: 2 playlist_item.where(playlist_id: id).command(:delete).call if collection
  37. 8 then: 6 else: 2 create_items record, collection if collection
  38. 8 record
  39. end
  40. end
  41. 1 def where(**)
  42. 4 playlist.where(**)
  43. 4 .order { created_at.asc }
  44. .to_a
  45. end
  46. 1 def with_items = with_current_item.combine :playlist_items
  47. 1 def with_screens = with_current_item.combine :screens
  48. 1 private
  49. 1 def with_current_item = playlist.combine current_item: :screen
  50. 1 def create_items playlist, collection
  51. 17 id = playlist.id
  52. 17 collection.map.with_index 1 do |item, position|
  53. 16 playlist_item.command(:create).call playlist_id: id,
  54. screen_id: item[:screen_id],
  55. position:
  56. end
  57. end
  58. end
  59. end
  60. end

app/repositories/playlist_item.rb

100.0% lines covered

100.0% branches covered

20 relevant lines. 20 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Repositories
  4. # The playlist repository.
  5. 1 class PlaylistItem < DB::Repository[:playlist_item]
  6. 1 commands :create, delete: :by_pk
  7. 1 commands update: :by_pk,
  8. use: :timestamps,
  9. plugins_options: {timestamps: {timestamps: :updated_at}}
  10. 1 def all
  11. 20 with_associations.order { [playlist_id, position.asc] }
  12. .to_a
  13. end
  14. 1 def create_with_position(offset: 1, **)
  15. 29 playlist_item.transaction do
  16. 29 playlist_item.command(:create)
  17. .call(position: playlist_item.count + offset, **)
  18. 29 .then { find it.id }
  19. end
  20. end
  21. 1 def delete_all(**) = playlist_item.where(**).delete
  22. 41 then: 38 else: 2 def find(id) = (with_associations.by_pk(id).one if id)
  23. 1 def find_by(**) = with_associations.where(**).one
  24. 1 def next_item(playlist_id:, after:) = playlist_item.next_item(playlist_id:, after:)
  25. 1 def where(**)
  26. 16 with_associations.where(**)
  27. 16 .order { [playlist_id, position.asc] }
  28. .to_a
  29. end
  30. 1 private
  31. 1 def with_associations = playlist_item.combine :playlist, :screen
  32. end
  33. end
  34. end

app/repositories/screen.rb

100.0% lines covered

100.0% branches covered

32 relevant lines. 32 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/core"
  3. 1 require "dry/monads"
  4. 1 module Terminus
  5. 1 module Repositories
  6. # The screen repository.
  7. 1 class Screen < DB::Repository[:screen]
  8. 1 include Dry::Monads[:result]
  9. 1 commands :create
  10. 1 commands update: :by_pk,
  11. use: :timestamps,
  12. plugins_options: {timestamps: {timestamps: :updated_at}}
  13. 1 def all
  14. 28 with_associations.order { updated_at.desc }
  15. .to_a
  16. end
  17. 1 def create_with_image path, mold, struct
  18. 110 path.open { |io| struct.upload io, metadata: {"filename" => mold.file_name} }
  19. 55 create image_data: struct.image_attributes, **mold.image_attributes
  20. end
  21. 1 def delete id
  22. 14 then: 6 else: 1 find(id).then { it.image_destroy if it }
  23. 7 screen.by_pk(id).delete
  24. end
  25. 30 then: 28 else: 1 def find(id) = (with_associations.by_pk(id).one if id)
  26. 1 def find_by(**) = with_associations.where(**).one
  27. 1 def search key, value
  28. 7 with_associations.where(Sequel.ilike(key, "%#{value}%"))
  29. 7 .order { created_at.asc }
  30. .to_a
  31. end
  32. 1 def upsert_with_image path, mold, struct
  33. 31 record = find_by name: mold.name, model_id: mold.model_id
  34. 31 then: 6 else: 25 record ? update_with_image(path, mold, record) : create_with_image(path, mold, struct)
  35. end
  36. 1 def where(**)
  37. 4 with_associations.where(**)
  38. 4 .order { created_at.asc }
  39. .to_a
  40. end
  41. 1 private
  42. 1 def with_associations = screen.combine :model
  43. 1 def update_with_image path, mold, record
  44. 12 path.open { |io| record.replace io, metadata: {"filename" => mold.file_name} }
  45. 6 update record.id, image_data: record.image_attributes, **mold.image_attributes
  46. end
  47. end
  48. end
  49. end

app/repositories/user.rb

100.0% lines covered

100.0% branches covered

17 relevant lines. 17 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Repositories
  4. # The user repository.
  5. 1 class User < DB::Repository[:user]
  6. 1 commands :create
  7. 1 commands update: :by_pk,
  8. use: :timestamps,
  9. plugins_options: {timestamps: {timestamps: :updated_at}}
  10. 1 def all
  11. 16 with_status.order { created_at.asc }
  12. .to_a
  13. end
  14. 16 then: 13 else: 2 def find(id) = (with_status.by_pk(id).one if id)
  15. 1 def find_by(**) = with_status.where(**).one
  16. 1 def search key, value
  17. 7 with_status.where(Sequel.ilike(key, "%#{value}%"))
  18. 7 .order { created_at.asc }
  19. .to_a
  20. end
  21. 1 def where(**)
  22. 4 with_status.where(**)
  23. 4 .order { created_at.asc }
  24. .to_a
  25. end
  26. 1 private
  27. 1 def with_status = user.combine :status
  28. end
  29. end
  30. end

app/repositories/user_status.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Repositories
  4. # The user repository.
  5. 1 class UserStatus < DB::Repository[:user_status]
  6. 1 def all = user_status.to_a
  7. 4 then: 2 else: 1 def find(id) = (user_status.by_pk(id).one if id)
  8. 1 def find_by(**) = user_status.where(**).one
  9. end
  10. end
  11. end

app/schemas/coercers/default_to_array.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 require "core"
  4. 1 require "refinements/hash"
  5. 1 module Terminus
  6. 1 module Schemas
  7. # Coerces a key's value to an empty array when key is missing.
  8. 1 module Coercers
  9. 1 using Refinements::Hash
  10. 1 DefaultToArray = lambda do |key, result, default = Core::EMPTY_ARRAY|
  11. 24 attributes = Hash result.to_h
  12. 24 else: 12 then: 12 attributes[key] = default unless result.key? key
  13. 24 attributes
  14. end
  15. end
  16. end
  17. end

app/schemas/coercers/default_to_false.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 require "refinements/hash"
  4. 1 module Terminus
  5. 1 module Schemas
  6. # Coerces a key's value to false when key is missing.
  7. 1 module Coercers
  8. 1 using Refinements::Hash
  9. 1 DefaultToFalse = lambda do |key, result|
  10. 49 else: 47 then: 2 return unless result.output
  11. 47 attributes = Hash result.to_h
  12. 47 else: 40 then: 7 attributes[key] = false unless result.key? key
  13. 47 attributes
  14. end
  15. end
  16. end
  17. end

app/schemas/coercers/json_to_hash.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 require "json"
  4. 1 require "refinements/hash"
  5. 1 module Terminus
  6. 1 module Schemas
  7. # Coerces a key's JSON value into a hash.
  8. 1 module Coercers
  9. 1 using Refinements::Hash
  10. 1 JSONToHash = lambda do |key, result|
  11. 112 attributes = Hash result.to_h
  12. 214 then: 53 else: 49 attributes.transform_value!(key) { JSON it if it }
  13. rescue JSON::ParserError
  14. 1 attributes
  15. end
  16. end
  17. end
  18. end

app/schemas/coercers/lines_to_array.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 require "refinements/hash"
  4. 1 module Terminus
  5. 1 module Schemas
  6. # Coerces a key's line delimited string value into an array.
  7. 1 module Coercers
  8. 1 using Refinements::Hash
  9. 1 LinesToArray = lambda do |key, result|
  10. 57 Hash(result.to_h).transform_value!(key) { String(it).split(/\r\n|\n|\r|\s/) }
  11. end
  12. end
  13. end
  14. end

app/schemas/coercers/uri_query_to_hash.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 require "json"
  4. 1 require "refinements/hash"
  5. 1 module Terminus
  6. 1 module Schemas
  7. # Coerces key's URI query parameters value into a hash.
  8. 1 module Coercers
  9. 1 using Refinements::Hash
  10. 1 URIQueryToHash = lambda do |key, result|
  11. 13 Hash(result.to_h).transform_value!(key) do |value|
  12. 11 else: 5 then: 6 Rack::Utils.parse_query value unless String(value).empty?
  13. end
  14. end
  15. end
  16. end
  17. end

app/schemas/devices/patch.rb

100.0% lines covered

100.0% branches covered

22 relevant lines. 22 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 module Terminus
  4. 1 module Schemas
  5. 1 module Devices
  6. # Defines device patch schema.
  7. 1 Patch = Dry::Schema.Params do
  8. 1 optional(:model_id).filled :integer
  9. 1 optional(:playlist_id).filled :integer
  10. 1 optional(:label).filled :string
  11. 1 optional(:friendly_id).filled :string
  12. 1 optional(:mac_address).filled Types::MACAddress
  13. 1 optional(:api_key).filled :string
  14. 1 optional(:refresh_rate).filled :integer, gt?: 0
  15. 1 optional(:image_timeout).filled :integer, gteq?: 0
  16. 1 optional(:firmware_update).filled :bool
  17. 1 optional(:firmware_version).filled Types::Version
  18. 1 optional(:battery_charge).filled :float, gteq?: 0
  19. 1 optional(:battery_voltage).filled :float, gteq?: 0
  20. 1 optional(:wifi).filled :integer
  21. 1 optional(:width).filled :integer
  22. 1 optional(:height).filled :integer
  23. 1 optional(:wake_reason).filled :string
  24. 1 optional(:sleep_start_at).maybe :string
  25. 1 optional(:sleep_stop_at).maybe :string
  26. end
  27. end
  28. end
  29. end

app/schemas/devices/sensors/upsert.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 module Terminus
  4. 1 module Schemas
  5. 1 module Devices
  6. 1 module Sensors
  7. # Defines device sensor upsert schema.
  8. 1 Upsert = Dry::Schema.Params do
  9. 1 required(:make).filled :string
  10. 1 required(:model).filled :string
  11. 1 required(:kind).filled :string
  12. 1 required(:value).filled :float
  13. 1 required(:unit).filled :string
  14. 1 required(:created_at).filled :integer
  15. end
  16. end
  17. end
  18. end
  19. end

app/schemas/devices/upsert.rb

100.0% lines covered

100.0% branches covered

23 relevant lines. 23 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 module Terminus
  4. 1 module Schemas
  5. 1 module Devices
  6. # Defines device upsert schema.
  7. 1 Upsert = Dry::Schema.Params do
  8. 1 required(:model_id).filled :integer
  9. 1 required(:playlist_id).maybe :integer
  10. 1 optional(:label).filled :string
  11. 1 optional(:friendly_id).filled :string
  12. 1 optional(:mac_address).filled Types::MACAddress
  13. 1 optional(:api_key).filled :string
  14. 1 optional(:refresh_rate).filled :integer, gt?: 0
  15. 1 optional(:image_timeout).filled :integer, gteq?: 0
  16. 1 optional(:firmware_update).filled :bool
  17. 1 optional(:firmware_version).filled Types::Version
  18. 1 optional(:battery_charge).filled :float, gteq?: 0
  19. 1 optional(:battery_voltage).filled :float
  20. 1 optional(:wifi).filled :integer
  21. 1 optional(:width).filled :integer
  22. 1 optional(:height).filled :integer
  23. 1 optional(:wake_reason).filled :string
  24. 1 optional(:sleep_start_at).maybe :string
  25. 1 optional(:sleep_stop_at).maybe :string
  26. 1 after(:value_coercer, &Coercers::DefaultToFalse.curry[:firmware_update])
  27. end
  28. end
  29. end
  30. end

app/schemas/extensions/exchanges/upsert.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 module Terminus
  4. 1 module Schemas
  5. 1 module Extensions
  6. 1 module Exchanges
  7. # Defines extension exchange upsert schema.
  8. 1 Upsert = Dry::Schema.Params do
  9. 1 optional(:headers).maybe :hash
  10. 1 optional(:verb).filled :string
  11. 1 required(:template).filled :string
  12. 1 optional(:body).maybe :hash
  13. 1 after(:value_coercer, &Coercers::JSONToHash.curry[:headers])
  14. 1 after(:value_coercer, &Coercers::JSONToHash.curry[:body])
  15. end
  16. end
  17. end
  18. end
  19. end

app/schemas/extensions/upsert.rb

100.0% lines covered

100.0% branches covered

27 relevant lines. 27 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 module Terminus
  4. 1 module Schemas
  5. 1 module Extensions
  6. # Defines extension upsert schema.
  7. 1 Upsert = Dry::Schema.Params do
  8. 1 optional(:model_ids).filled :array
  9. 1 optional(:device_ids).filled :array
  10. 1 required(:name).filled :string
  11. 1 required(:label).filled :string
  12. 1 required(:description).maybe :string
  13. 1 optional(:mode).filled :string
  14. 1 required(:kind).filled :string
  15. 1 required(:tags).maybe :array
  16. 1 required(:static_body).maybe :hash
  17. 1 required(:template).maybe :string
  18. 1 required(:fields).maybe :array
  19. 1 required(:data).maybe :hash
  20. 1 required(:interval).maybe :integer
  21. 1 optional(:unit).filled :string
  22. 1 optional(:days).maybe :array
  23. 1 required(:last_day_of_month).filled :bool
  24. 1 required(:start_at).filled :date_time
  25. 1 after(:value_coercer, &Coercers::LinesToArray.curry[:tags])
  26. 1 after(:value_coercer, &Coercers::DefaultToFalse.curry[:last_day_of_month])
  27. 1 after(:value_coercer, &Coercers::DefaultToArray.curry[:days])
  28. 1 after(:value_coercer, &Coercers::JSONToHash.curry[:static_body])
  29. 1 after(:value_coercer, &Coercers::JSONToHash.curry[:fields])
  30. 1 after(:value_coercer, &Coercers::JSONToHash.curry[:data])
  31. end
  32. end
  33. end
  34. end

app/schemas/firmware/header.rb

100.0% lines covered

100.0% branches covered

18 relevant lines. 18 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Schemas
  4. 1 module Firmware
  5. # Validates request headers.
  6. 1 Header = Dry::Schema.Params do
  7. 1 optional(:HTTP_ACCESS_TOKEN).maybe :string
  8. 1 optional(:HTTP_BATTERY_VOLTAGE).filled :float
  9. 1 optional(:HTTP_FW_VERSION).filled Types::Version
  10. 1 optional(:HTTP_HEIGHT).filled :integer
  11. 1 optional(:HTTP_HOST).filled :string
  12. 1 required(:HTTP_ID).filled Types::MACAddress
  13. 1 optional(:HTTP_MODEL).filled :string
  14. 1 optional(:HTTP_PERCENT_CHARGED).filled :float
  15. 1 optional(:HTTP_REFRESH_RATE).filled :integer
  16. 1 optional(:HTTP_RSSI).filled :integer
  17. 1 optional(:HTTP_SENSORS).maybe :string
  18. 1 optional(:HTTP_UPDATE_SOURCE).filled :string
  19. 1 optional(:HTTP_USER_AGENT).filled :string
  20. 1 optional(:HTTP_WIDTH).filled :integer
  21. end
  22. end
  23. end
  24. end

app/schemas/models/upsert.rb

100.0% lines covered

100.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 module Terminus
  4. 1 module Schemas
  5. 1 module Models
  6. # Defines model upsert schema.
  7. 1 Upsert = Dry::Schema.Params do
  8. 1 required(:name).filled :string
  9. 1 required(:label).filled :string
  10. 1 optional(:description).maybe :string
  11. 1 optional(:default_palette_id).maybe :integer
  12. 1 optional(:mime_type).filled :string
  13. 1 optional(:colors).filled :integer
  14. 1 optional(:bit_depth).filled :integer
  15. 1 optional(:rotation).filled :integer
  16. 1 optional(:offset_x).filled :integer
  17. 1 optional(:offset_y).filled :integer
  18. 1 optional(:scale_factor).filled :float
  19. 1 optional(:width).filled :integer
  20. 1 optional(:height).filled :integer
  21. 1 optional(:css).maybe :hash
  22. 1 after(:value_coercer, &Coercers::JSONToHash.curry[:css])
  23. end
  24. end
  25. end
  26. end

app/serializers/device.rb

100.0% lines covered

100.0% branches covered

14 relevant lines. 14 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 module Terminus
  4. 1 module Serializers
  5. # A device serializer for specific keys.
  6. 1 class Device
  7. 1 KEYS = %i[
  8. id
  9. model_id
  10. playlist_id
  11. friendly_id
  12. label
  13. mac_address
  14. api_key
  15. firmware_version
  16. wifi
  17. battery_charge
  18. battery_voltage
  19. refresh_rate
  20. image_timeout
  21. wake_reason
  22. width
  23. height
  24. firmware_update
  25. sleep_start_at
  26. sleep_stop_at
  27. created_at
  28. updated_at
  29. synced_at
  30. ].freeze
  31. 1 def initialize record, keys: KEYS, transformer: Transformers::Time
  32. 8 @record = record
  33. 8 @keys = keys
  34. 8 @transformer = transformer
  35. end
  36. 1 def to_h
  37. 8 attributes = record.to_h.slice(*keys)
  38. 8 attributes.transform_values!(&transformer)
  39. 8 attributes
  40. end
  41. 1 private
  42. 1 attr_reader :record, :keys, :transformer
  43. end
  44. end
  45. end

app/serializers/firmware.rb

100.0% lines covered

100.0% branches covered

16 relevant lines. 16 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 module Terminus
  4. 1 module Serializers
  5. # A model serializer for specific keys.
  6. 1 class Firmware
  7. 1 KEYS = %i[id version kind created_at updated_at].freeze
  8. 1 def initialize record, keys: KEYS, transformer: Transformers::Time
  9. 8 @record = record
  10. 8 @keys = keys
  11. 8 @transformer = transformer
  12. end
  13. 1 def to_h
  14. 8 attributes = record.to_h.slice(*keys).merge(file_name: "#{record.version}.bin")
  15. 8 attributes.transform_values!(&transformer)
  16. 8 then: 7 else: 1 attributes.merge! metadata, uri: record.attachment_uri if record.attachment_id
  17. 8 attributes
  18. end
  19. 1 private
  20. 1 attr_reader :record, :keys, :transformer
  21. 1 def metadata = record.attachment_attributes[:metadata].slice :mime_type, :size
  22. end
  23. end
  24. end

app/serializers/model.rb

100.0% lines covered

100.0% branches covered

14 relevant lines. 14 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 module Terminus
  4. 1 module Serializers
  5. # A model serializer for specific keys.
  6. 1 class Model
  7. 1 KEYS = %i[
  8. default_palette_id
  9. id
  10. name
  11. label
  12. description
  13. kind
  14. mime_type
  15. colors
  16. bit_depth
  17. rotation
  18. offset_x
  19. offset_y
  20. scale_factor
  21. css
  22. width
  23. height
  24. created_at
  25. updated_at
  26. ].freeze
  27. 1 def initialize record, keys: KEYS, transformer: Transformers::Time
  28. 7 @record = record
  29. 7 @keys = keys
  30. 7 @transformer = transformer
  31. end
  32. 1 def to_h
  33. 7 attributes = record.to_h.slice(*keys)
  34. 7 attributes.transform_values!(&transformer)
  35. 7 attributes
  36. end
  37. 1 private
  38. 1 attr_reader :record, :keys, :transformer
  39. end
  40. end
  41. end

app/serializers/playlist.rb

100.0% lines covered

100.0% branches covered

22 relevant lines. 22 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 require "core"
  4. 1 require "initable"
  5. 1 module Terminus
  6. 1 module Serializers
  7. # A playlist serializer for specific keys.
  8. 1 class Playlist
  9. 1 include Initable[
  10. keys: %i[id name label current_item_id mode created_at updated_at],
  11. item_serializer: PlaylistItem
  12. ]
  13. 1 def initialize(record, transformer: Transformers::Time, **)
  14. 10 super(**)
  15. 10 @record = record
  16. 10 @keys = keys
  17. 10 @transformer = transformer
  18. end
  19. 1 def to_h
  20. 10 else: 9 then: 1 return Core::EMPTY_HASH unless record
  21. 9 attributes = record.to_h.slice(*keys)
  22. 9 attributes.transform_values!(&transformer)
  23. 9 attributes[:items] = items
  24. 9 attributes
  25. end
  26. 1 private
  27. 1 attr_reader :record, :keys, :transformer
  28. 1 def items
  29. 16 record.playlist_items.map { item_serializer.new(it).to_h }
  30. rescue NoMethodError, ROM::Struct::MissingAttribute
  31. 1 Core::EMPTY_ARRAY
  32. end
  33. end
  34. end
  35. end

app/serializers/playlist_item.rb

100.0% lines covered

100.0% branches covered

14 relevant lines. 14 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 module Terminus
  4. 1 module Serializers
  5. # A playlist item serializer for specific keys.
  6. 1 class PlaylistItem
  7. 1 KEYS = %i[id screen_id position created_at updated_at].freeze
  8. 1 def initialize record, keys: KEYS, transformer: Transformers::Time
  9. 8 @record = record
  10. 8 @keys = keys
  11. 8 @transformer = transformer
  12. end
  13. 1 def to_h
  14. 8 attributes = record.to_h.slice(*keys)
  15. 8 attributes.transform_values!(&transformer)
  16. 8 attributes
  17. end
  18. 1 private
  19. 1 attr_reader :record, :keys, :transformer
  20. end
  21. end
  22. end

app/serializers/screen.rb

100.0% lines covered

100.0% branches covered

17 relevant lines. 17 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 module Terminus
  4. 1 module Serializers
  5. # A screen serializer for specific keys.
  6. 1 class Screen
  7. 1 KEYS = %i[id model_id label name created_at updated_at].freeze
  8. 1 def initialize record, keys: KEYS, transformer: Transformers::Time
  9. 10 @record = record
  10. 10 @keys = keys
  11. 10 @transformer = transformer
  12. end
  13. 1 def to_h
  14. 10 attributes = record.to_h.slice(*keys)
  15. 10 attributes.transform_values!(&transformer)
  16. 10 then: 9 else: 1 attributes.merge! metadata, uri: record.image_uri if record.image_id
  17. 10 attributes
  18. end
  19. 1 private
  20. 1 attr_reader :record, :keys, :transformer
  21. 1 def metadata
  22. 9 record.image_attributes[:metadata].slice :filename,
  23. :mime_type,
  24. :bit_depth,
  25. :width,
  26. :height,
  27. :size
  28. end
  29. end
  30. end
  31. end

app/serializers/transformers/time.rb

100.0% lines covered

100.0% branches covered

8 relevant lines. 8 lines covered and 0 lines missed.
3 total branches, 3 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 module Terminus
  4. 1 module Serializers
  5. 1 module Transformers
  6. # Transforms SQL time to a string.
  7. 1 Time = lambda do |value|
  8. 477 when: 3 case value
  9. 3 when: 89 when Sequel::SQLTime then value.to_s
  10. 89 else: 385 when ::Time then value.strftime "%Y-%m-%dT%H:%M:%S%z"
  11. 385 else value
  12. end
  13. end
  14. end
  15. end
  16. end

app/structs/device.rb

100.0% lines covered

100.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 module Terminus
  4. 1 module Structs
  5. # The device struct.
  6. 1 class Device < DB::Struct
  7. 1 def as_api_display
  8. 7 {image_url_timeout: image_timeout, refresh_rate:, update_firmware: firmware_update}
  9. end
  10. 1 def asleep? at = Time.now, type: Sequel::SQLTime
  11. 40 else: 4 then: 36 return false unless sleep_start_at && sleep_stop_at
  12. 4 now = type.create at.hour, at.min, at.sec
  13. 4 then: 2 if sleep_stop_at < sleep_start_at
  14. 2 now >= sleep_start_at || now <= sleep_stop_at
  15. else: 2 else
  16. 2 (sleep_start_at..sleep_stop_at).cover? now
  17. end
  18. end
  19. 1 def slug
  20. 2 else: 1 then: 1 return Core::EMPTY_STRING unless mac_address
  21. 1 mac_address.tr ":", Core::EMPTY_STRING
  22. end
  23. 1 def screen_label(prefix) = "#{prefix} #{friendly_id}"
  24. 1 def screen_name(kind) = "terminus_#{kind}_#{friendly_id.downcase}"
  25. 1 def screen_attributes kind
  26. {
  27. 35 model_id:,
  28. name: screen_name(kind),
  29. label: screen_label(kind.capitalize)
  30. }
  31. end
  32. end
  33. end
  34. end

app/structs/device_sensor.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "refinements/hash"
  3. 1 module Terminus
  4. 1 module Structs
  5. # The device sensor struct.
  6. 1 class DeviceSensor < DB::Struct
  7. 1 using Refinements::Hash
  8. 1 def liquid_attributes
  9. 4 {device_id:, make:, model:, kind:, value:, unit:, source:, created_at:}.stringify_keys!
  10. end
  11. end
  12. end
  13. end

app/structs/extension.rb

100.0% lines covered

100.0% branches covered

26 relevant lines. 26 lines covered and 0 lines missed.
5 total branches, 5 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/core"
  3. 1 require "refinements/time"
  4. 1 module Terminus
  5. 1 module Structs
  6. # The extension struct.
  7. 1 class Extension < DB::Struct
  8. 1 WEEK = %w[sunday monday tuesday wednesday thursday friday saturday].freeze
  9. 1 using Refinements::Time
  10. 1 def export_attributes
  11. {
  12. 4 name:,
  13. label:,
  14. description:,
  15. mode:,
  16. kind:,
  17. tags:,
  18. static_body:,
  19. fields:,
  20. template:,
  21. data:,
  22. interval:,
  23. unit:,
  24. days:,
  25. last_day_of_month:,
  26. start_at: start_at.rfc_3339
  27. }
  28. end
  29. 1 def liquid_attributes
  30. 43 all_fields = Array fields
  31. 43 values = all_fields.each.with_object({}) do |item, all|
  32. 19 key, value = item.values_at "keyname", "default"
  33. 19 all[key] = Hash(data).dig("values", key) || value
  34. end
  35. 43 {"label" => label, "fields" => all_fields, "values" => values, "data" => data}
  36. end
  37. 1 def screen_label = "Extension #{label}"
  38. 1 def screen_name = "extension-#{name}"
  39. 1 def screen_attributes = {label: screen_label, name: screen_name, mode:}
  40. 1 def to_cron croner: Aspects::Croner, week: WEEK
  41. 14 in: 1 case self
  42. 3 in unit: "week" then croner.call days.map { week.index it }, unit, time: start_at
  43. in: 1 in unit: "month", last_day_of_month: true
  44. 1 else: 12 croner.call "#{interval}L", unit, time: start_at
  45. 12 else croner.call interval, unit, time: start_at
  46. end
  47. end
  48. 1 def to_schedule
  49. 20 then: 10 else: 10 return [screen_name, Core::EMPTY_HASH] if unit == "none"
  50. [
  51. 10 screen_name,
  52. {
  53. cron: to_cron,
  54. class: Terminus::Jobs::Batches::Extension.name,
  55. args: [id],
  56. description: "The #{label} extension update schedule."
  57. }
  58. ]
  59. end
  60. end
  61. end
  62. end

app/structs/extension_exchange.rb

100.0% lines covered

100.0% branches covered

5 relevant lines. 5 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Structs
  4. # The extension exchange struct.
  5. 1 class ExtensionExchange < DB::Struct
  6. 1 def export_attributes = {headers:, verb:, body:, template:}
  7. 1 def http_attributes = {headers:, verb:, body:}
  8. end
  9. end
  10. end

app/structs/firmware.rb

100.0% lines covered

100.0% branches covered

36 relevant lines. 36 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "refinements/hash"
  3. 1 module Terminus
  4. 1 module Structs
  5. # The firmware struct.
  6. 1 class Firmware < DB::Struct
  7. 1 include Terminus::Uploaders::Binary::Attachment[:attachment]
  8. 1 using Refinements::Hash
  9. 1 attr_reader :attachment_data
  10. 1 def initialize(*, store: Hanami.app[:shrine].storages[:store])
  11. 132 super(*)
  12. 132 @store = store
  13. 132 @attacher = attachment_attacher
  14. end
  15. 1 def attachment_attributes = attributes[:attachment_data].deep_symbolize_keys
  16. 1 def attachment_destroy
  17. 12 then: 10 else: 2 store.delete attachment_id if attachment_id
  18. 12 attributes[:attachment_data].clear
  19. end
  20. 1 def attachment_id = attachment_attributes[:id]
  21. 1 def attachment_name = attachment_attributes.dig :metadata, :filename
  22. 1 def attachment_open(**)
  23. 2 io = store.open(attachment_id, **)
  24. 2 then: 1 else: 1 yield io if block_given?
  25. ensure
  26. 2 io.close
  27. end
  28. 1 def attachment_size = attachment_attributes.dig :metadata, :size
  29. 1 def attachment_type = attachment_attributes.dig :metadata, :mime_type
  30. 25 then: 21 else: 3 def attachment_uri(**) = (store.url(attachment_id, **) if attachment_id)
  31. 1 def attach(io, **)
  32. 12 attacher.assign(io, **).tap { |file| attributes[:attachment_data] = file.data }
  33. end
  34. 1 def replace(io, **)
  35. 4 attachment_destroy
  36. 4 upload(io, **)
  37. 4 self
  38. end
  39. 1 def upload(io, **)
  40. 24 attacher.upload(io, **).tap { |file| attributes[:attachment_data] = file.data }
  41. end
  42. 1 def errors = attacher.errors
  43. 1 def valid? = errors.empty?
  44. 1 private
  45. 1 attr_reader :attacher, :store
  46. end
  47. end
  48. end

app/structs/model.rb

100.0% lines covered

100.0% branches covered

8 relevant lines. 8 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Structs
  4. # The model struct.
  5. 1 class Model < DB::Struct
  6. 1 def css_classes
  7. 19 size = css.dig "classes", "size"
  8. 19 density = css.dig "classes", "density"
  9. 19 "screen screen--#{name} screen--#{bit_depth}bit screen--#{orientation} " \
  10. "#{size} screen--1x #{density}".strip.squeeze " "
  11. end
  12. 22 then: 20 else: 1 def orientation = rotation.zero? ? "landscape" : "portrait"
  13. end
  14. end
  15. end

app/structs/palette.rb

100.0% lines covered

100.0% branches covered

5 relevant lines. 5 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/core"
  3. 1 module Terminus
  4. 1 module Structs
  5. # The palette struct.
  6. 1 class Palette < DB::Struct
  7. 1 def screen_attributes = {grays:, color_codes: colors}
  8. end
  9. end
  10. end

app/structs/playlist.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Structs
  4. # The playlist struct.
  5. 1 class Playlist < DB::Struct
  6. 1 def automatic? = mode == "automatic"
  7. 11 then: 7 else: 3 def current_item_position(default: 1) = current_item ? current_item.position : default
  8. 1 def manual? = mode == "manual"
  9. end
  10. end
  11. end

app/structs/playlist_item.rb

100.0% lines covered

100.0% branches covered

5 relevant lines. 5 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Structs
  4. # The playlist item struct.
  5. 1 class PlaylistItem < DB::Struct
  6. 1 def cloneable_attributes
  7. {
  8. 9 screen_id:,
  9. position:,
  10. repeat_interval:,
  11. repeat_type:,
  12. repeat_days:,
  13. last_day_of_month:,
  14. start_at:,
  15. stop_at:,
  16. hidden_at:
  17. }
  18. end
  19. end
  20. end
  21. end

app/structs/screen.rb

100.0% lines covered

100.0% branches covered

46 relevant lines. 46 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "refinements/hash"
  3. 1 module Terminus
  4. 1 module Structs
  5. # The screen struct.
  6. # :reek:TooManyMethods
  7. 1 class Screen < DB::Struct
  8. 1 include Terminus::Uploaders::Image::Attachment[:image]
  9. 1 using Refinements::Hash
  10. 1 attr_reader :image_data
  11. 1 def initialize(*, store: Hanami.app[:shrine].storages[:store])
  12. 851 super(*)
  13. 851 @store = store
  14. 851 @attacher = image_attacher
  15. end
  16. 1 def bit_depth = image_attributes.dig :metadata, :bit_depth
  17. 1 def height = image_attributes.dig :metadata, :height
  18. 1 def image_attributes = attributes[:image_data].deep_symbolize_keys
  19. 1 def image_destroy
  20. 19 then: 11 else: 8 store.delete image_id if image_id
  21. 19 attributes[:image_data].clear
  22. end
  23. 1 def image_id = image_attributes[:id]
  24. 1 def image_name = image_attributes.dig :metadata, :filename
  25. 1 def image_name_with_checksum
  26. 5 path = Pathname(image_attributes.dig(:metadata, :filename))
  27. 5 extension = path.extname
  28. 5 path.sub_ext("-#{image_attributes.dig :metadata, :checksum}#{extension}").to_s
  29. end
  30. 1 def image_open(**)
  31. 2 io = store.open(image_id, **)
  32. 2 then: 1 else: 1 yield io if block_given?
  33. ensure
  34. 2 io.close
  35. end
  36. 1 def image_size = image_attributes.dig :metadata, :size
  37. 87 then: 66 else: 20 def image_uri(**) = (store.url(image_id, **) if image_id)
  38. 1 def attach(io, **)
  39. 18 attacher.assign(io, **).tap { |file| attributes[:image_data] = file.data }
  40. 9 self
  41. end
  42. 1 def mime_type = image_attributes.dig :metadata, :mime_type
  43. 1 def replace(io, **)
  44. 11 image_destroy
  45. 11 upload(io, **)
  46. 11 self
  47. end
  48. 1 def upload(io, **)
  49. 202 attacher.upload(io, **).tap { |file| attributes[:image_data] = file.data }
  50. 101 self
  51. end
  52. 1 def errors = attacher.errors
  53. 1 def valid? = errors.empty?
  54. 1 def width = image_attributes.dig :metadata, :width
  55. 1 def popover_attributes = {id:, label:, uri: image_uri, width:, height:}
  56. 1 private
  57. 1 attr_reader :store, :attacher
  58. end
  59. end
  60. end

app/uploaders/binary.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 module Terminus
  4. 1 module Uploaders
  5. # Processes binary uploads.
  6. 1 class Binary < Hanami.app[:shrine]
  7. 1 Attacher.validate do
  8. 8 validate_mime_type ["application/octet-stream"]
  9. 8 validate_extension ["bin"]
  10. end
  11. end
  12. end
  13. end

app/uploaders/image.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 module Terminus
  4. 1 module Uploaders
  5. # Processes image uploads.
  6. 1 class Image < Hanami.app[:shrine]
  7. 1 add_metadata :bit_depth do |io|
  8. 117 then: 109 else: 8 MiniMagick::Image.open(io.path).data["depth"] if io.respond_to? :path
  9. end
  10. 118 add_metadata(:checksum) { |io| calculate_signature io, :md5 }
  11. 1 Attacher.validate do
  12. 16 validate_mime_type %w[image/bmp image/png]
  13. 16 validate_extension %w[bmp png]
  14. end
  15. end
  16. end
  17. end

app/view.rb

100.0% lines covered

100.0% branches covered

3 relevant lines. 3 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 require "hanami/view"
  4. 1 module Terminus
  5. # The application base view.
  6. 1 class View < Hanami::View
  7. end
  8. end

app/views/bulk/devices/logs/delete.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Bulk
  5. 1 module Devices
  6. 1 module Logs
  7. # The delete view.
  8. 1 class Delete < View
  9. end
  10. end
  11. end
  12. end
  13. end
  14. end

app/views/bulk/firmware/delete.rb

100.0% lines covered

100.0% branches covered

5 relevant lines. 5 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Bulk
  5. 1 module Firmware
  6. # The delete view.
  7. 1 class Delete < View
  8. end
  9. end
  10. end
  11. end
  12. end

app/views/context.rb

100.0% lines covered

100.0% branches covered

8 relevant lines. 8 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 require "hanami/view"
  4. 1 module Terminus
  5. 1 module Views
  6. # The application custom view context.
  7. 1 class Context < Hanami::View::Context
  8. 1 include Deps[:htmx, :htmx_defaults]
  9. 1 def htmx? = htmx.request? request.env, :request, "true"
  10. 1 def htmx_configuration
  11. 374 then: 3 else: 184 content_for(:htmx_merge).then { it ? htmx_defaults.merge(it) : htmx_defaults }
  12. .to_json
  13. end
  14. end
  15. end
  16. end

app/views/dashboard/show.rb

100.0% lines covered

100.0% branches covered

15 relevant lines. 15 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Dashboard
  5. # The show view.
  6. 1 class Show < View
  7. 1 include Deps[
  8. device_relation: "relations.device",
  9. extension_relation: "relations.extension",
  10. firmware_relation: "relations.firmware",
  11. model_relation: "relations.model",
  12. playlist_relation: "relations.playlist",
  13. screen_relation: "relations.screen",
  14. user_relation: "relations.user"
  15. ]
  16. 1 expose :api_uri
  17. 1 expose :ip_addresses
  18. 1 expose :firmware
  19. 56 expose(:device_count) { device_relation.count }
  20. 56 expose(:extension_count) { extension_relation.count }
  21. 56 expose(:firmware_count) { firmware_relation.count }
  22. 56 expose(:model_count) { model_relation.count }
  23. 56 expose(:playlist_count) { playlist_relation.count }
  24. 56 expose(:screen_count) { screen_relation.count }
  25. 56 expose(:user_count) { user_relation.count }
  26. end
  27. end
  28. end
  29. end

app/views/designer/show.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Designer
  5. # The show view.
  6. 1 class Show < View
  7. 1 expose :name, default: :terminus_designer
  8. 1 expose :label, default: "Designer"
  9. end
  10. end
  11. end
  12. end

app/views/devices/edit.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 module Terminus
  4. 1 module Views
  5. 1 module Devices
  6. # The edit view.
  7. 1 class Edit < View
  8. 1 expose :models
  9. 1 expose :playlists
  10. 1 expose :device
  11. 1 expose :fields, default: Core::EMPTY_HASH
  12. 1 expose :errors, default: Core::EMPTY_HASH
  13. end
  14. end
  15. end
  16. end

app/views/devices/index.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Devices
  5. # The index view.
  6. 1 class Index < View
  7. 1 expose :devices
  8. 1 expose :query
  9. end
  10. end
  11. end
  12. end

app/views/devices/logs/index.rb

100.0% lines covered

100.0% branches covered

8 relevant lines. 8 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Devices
  5. 1 module Logs
  6. # The index view.
  7. 1 class Index < View
  8. 1 expose :device
  9. 1 expose :logs
  10. 1 expose :query
  11. end
  12. end
  13. end
  14. end
  15. end

app/views/devices/logs/show.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Devices
  5. 1 module Logs
  6. # The show view.
  7. 1 class Show < View
  8. 1 expose :device
  9. 1 expose :log
  10. end
  11. end
  12. end
  13. end
  14. end

app/views/devices/new.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 module Terminus
  4. 1 module Views
  5. 1 module Devices
  6. # The new view.
  7. 1 class New < View
  8. 1 expose :models
  9. 1 expose :playlists
  10. 1 expose :device
  11. 1 expose :fields, default: Core::EMPTY_HASH
  12. 1 expose :errors, default: Core::EMPTY_HASH
  13. end
  14. end
  15. end
  16. end

app/views/devices/show.rb

100.0% lines covered

100.0% branches covered

5 relevant lines. 5 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Devices
  5. # The show view.
  6. 1 class Show < View
  7. 1 expose :device
  8. end
  9. end
  10. end
  11. end

app/views/extensions/build/new.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Extensions
  5. 1 module Build
  6. # The new view.
  7. 1 class New < View
  8. 1 expose :extension
  9. end
  10. end
  11. end
  12. end
  13. end

app/views/extensions/clone/new.rb

100.0% lines covered

100.0% branches covered

5 relevant lines. 5 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Extensions
  5. 1 module Clone
  6. # The new view.
  7. 1 class New < Views::Extensions::New
  8. end
  9. end
  10. end
  11. end
  12. end

app/views/extensions/dynamic.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Extensions
  5. # The dynamic view.
  6. 1 class Dynamic < View
  7. 1 config.layout = "extension"
  8. 1 expose :content
  9. end
  10. end
  11. end
  12. end

app/views/extensions/edit.rb

100.0% lines covered

100.0% branches covered

13 relevant lines. 13 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 module Terminus
  4. 1 module Views
  5. 1 module Extensions
  6. # The edit view.
  7. 1 class Edit < View
  8. 1 include Deps[
  9. model_repository: "repositories.model",
  10. device_repository: "repositories.device",
  11. exchange_repository: "repositories.extension_exchange"
  12. ]
  13. 7 expose(:default_model) { model_repository.find_by name: "og_plus" }
  14. 13 expose(:models) { model_repository.all.map { [it.label, it.id] } }
  15. 7 expose(:devices) { device_repository.all.map { [it.label, it.id] } }
  16. 7 expose(:exchanges) { |extension:| exchange_repository.where extension_id: extension.id }
  17. 1 expose :extension
  18. 1 expose :fields, default: Core::EMPTY_HASH
  19. 1 expose :errors, default: Core::EMPTY_HASH
  20. end
  21. end
  22. end
  23. end

app/views/extensions/exchanges/edit.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 module Terminus
  4. 1 module Views
  5. 1 module Extensions
  6. 1 module Exchanges
  7. # The edit view.
  8. 1 class Edit < View
  9. 1 expose :extension
  10. 1 expose :exchange
  11. 1 expose :fields, default: Core::EMPTY_HASH
  12. 1 expose :errors, default: Core::EMPTY_HASH
  13. end
  14. end
  15. end
  16. end
  17. end

app/views/extensions/exchanges/index.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Extensions
  5. 1 module Exchanges
  6. # The index view.
  7. 1 class Index < View
  8. 1 expose :extension
  9. 1 expose :exchanges
  10. end
  11. end
  12. end
  13. end
  14. end

app/views/extensions/exchanges/new.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 module Terminus
  4. 1 module Views
  5. 1 module Extensions
  6. 1 module Exchanges
  7. # The new view.
  8. 1 class New < View
  9. 1 expose :extension
  10. 1 expose :exchange
  11. 1 expose :fields, default: Core::EMPTY_HASH
  12. 1 expose :errors, default: Core::EMPTY_HASH
  13. end
  14. end
  15. end
  16. end
  17. end

app/views/extensions/gallery/index.rb

100.0% lines covered

100.0% branches covered

8 relevant lines. 8 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Extensions
  5. 1 module Gallery
  6. # The index view.
  7. 1 class Index < Hanami::View
  8. 1 expose :recipe
  9. 1 expose :query, decorate: false
  10. 1 expose :page, decorate: false
  11. end
  12. end
  13. end
  14. end
  15. end

app/views/extensions/index.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Extensions
  5. # The index view.
  6. 1 class Index < Hanami::View
  7. 1 expose :extensions
  8. 1 expose :query
  9. end
  10. end
  11. end
  12. end

app/views/extensions/new.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 module Terminus
  4. 1 module Views
  5. 1 module Extensions
  6. # The new view.
  7. 1 class New < View
  8. 1 include Deps[
  9. model_repository: "repositories.model",
  10. device_repository: "repositories.device"
  11. ]
  12. 11 expose(:models) { model_repository.all.map { [it.label, it.id] } }
  13. 8 expose(:devices) { device_repository.all.map { [it.label, it.id] } }
  14. 1 expose :extension
  15. 1 expose :fields, default: Core::EMPTY_HASH
  16. 1 expose :errors, default: Core::EMPTY_HASH
  17. end
  18. end
  19. end
  20. end

app/views/extensions/sensors/index.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Extensions
  5. 1 module Sensors
  6. # The show view.
  7. 1 class Index < View
  8. 1 expose :content
  9. end
  10. end
  11. end
  12. end
  13. end

app/views/extensions/sources/index.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Extensions
  5. 1 module Sources
  6. # The index view.
  7. 1 class Index < View
  8. 1 expose :content
  9. end
  10. end
  11. end
  12. end
  13. end

app/views/firmware/edit.rb

100.0% lines covered

100.0% branches covered

8 relevant lines. 8 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 module Terminus
  4. 1 module Views
  5. 1 module Firmware
  6. # The edit view.
  7. 1 class Edit < View
  8. 1 expose :firmware
  9. 1 expose :fields, default: Core::EMPTY_HASH
  10. 1 expose :errors, default: Core::EMPTY_HASH
  11. end
  12. end
  13. end
  14. end

app/views/firmware/index.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Firmware
  5. # The index view.
  6. 1 class Index < View
  7. 1 expose :firmware
  8. 1 expose :query
  9. end
  10. end
  11. end
  12. end

app/views/firmware/new.rb

100.0% lines covered

100.0% branches covered

8 relevant lines. 8 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 module Terminus
  4. 1 module Views
  5. 1 module Firmware
  6. # The new view.
  7. 1 class New < View
  8. 1 expose :firmware
  9. 1 expose :fields, default: Core::EMPTY_HASH
  10. 1 expose :errors, default: Core::EMPTY_HASH
  11. end
  12. end
  13. end
  14. end

app/views/firmware/show.rb

100.0% lines covered

100.0% branches covered

5 relevant lines. 5 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Firmware
  5. # The show view.
  6. 1 class Show < View
  7. 1 expose :firmware
  8. end
  9. end
  10. end
  11. end

app/views/helpers.rb

100.0% lines covered

100.0% branches covered

47 relevant lines. 47 lines covered and 0 lines missed.
14 total branches, 14 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 require "htmx"
  4. 1 require "refinements/hash"
  5. 1 require "refinements/string"
  6. 1 module Terminus
  7. 1 module Views
  8. # The view helpers.
  9. 1 module Helpers
  10. 1 extend Hanami::View::Helpers::TagHelper
  11. 1 using Refinements::Hash
  12. 1 using Refinements::String
  13. 1 module_function
  14. 1 def boolean value
  15. 6 then: 5 else: 1 css_class = value == true ? "bit-text-green" : "bit-text-red"
  16. 6 tag.span value.to_s, class: css_class
  17. end
  18. # rubocop:todo Metrics/ParameterLists
  19. 1 def field_included? key, value, attributes, record = nil
  20. 94 ((record && record.public_send(key)) || attributes[key]).include? value
  21. end
  22. # rubocop:enable Metrics/ParameterLists
  23. 1 def field_for key, attributes, record = nil
  24. 538 else: 326 then: 212 return attributes[key] unless record
  25. 326 value = attributes.fetch_value key, record.public_send(key)
  26. 326 when: 1 case value
  27. 1 when: 7 when Sequel::SQLTime then value.strftime("%H:%M:%S")
  28. 7 else: 318 when Time then value.strftime("%Y-%m-%dT%H:%M")
  29. 318 else value
  30. end
  31. end
  32. 1 def git_link kernel: Kernel
  33. 269 settings = Hanami.app[:settings]
  34. 269 tag_sha = kernel.`("git rev-parse --quiet --short #{settings.git_tag}^{}").strip
  35. 269 then: 1 else: 268 tag_sha == settings.git_latest_sha ? git_version_link : git_latest_link
  36. end
  37. 1 def git_latest_link
  38. 269 settings = Hanami.app[:settings]
  39. 269 link_to "Latest (ahead of #{settings.git_tag})",
  40. "https://github.com/usetrmnl/terminus/commit/#{settings.git_latest_sha}",
  41. class: :link
  42. end
  43. 1 def git_version_link
  44. 2 tag = Hanami.app[:settings].git_tag
  45. 2 link_to "Version #{tag}",
  46. "https://github.com/usetrmnl/terminus/releases/tag/#{tag}",
  47. class: :link
  48. end
  49. 59 then: 53 else: 5 def human_at(value) = (value.strftime "%B %d %Y at %H:%M %Z" if value)
  50. 11 then: 1 else: 9 def human_time(value) = (value.strftime "%I:%M %p" if value)
  51. 1 def pluralize value, suffix, count = 0
  52. 62 %(#{count} #{value.pluralize suffix, count})
  53. end
  54. 1 def select_options_for records, label: :label, id: :id
  55. 37 records.reduce [["Select...", Core::EMPTY_STRING]] do |options, record|
  56. 36 options.append [record.public_send(label), record.public_send(id)]
  57. end
  58. end
  59. 1 def size value, kilobyte: 1_024, units: %w[B KB MB GB TB]
  60. 26 bytes = value.to_f
  61. 26 index = 0
  62. 26 body: 11 while bytes >= kilobyte && index < units.length - 1
  63. 11 bytes /= kilobyte
  64. 11 index += 1
  65. end
  66. 26 "#{bytes.round 2} #{units[index]}"
  67. end
  68. end
  69. end
  70. end

app/views/models/clone/new.rb

100.0% lines covered

100.0% branches covered

5 relevant lines. 5 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Models
  5. 1 module Clone
  6. # The new view.
  7. 1 class New < Models::New
  8. end
  9. end
  10. end
  11. end
  12. end

app/views/models/edit.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 module Terminus
  4. 1 module Views
  5. 1 module Models
  6. # The edit view.
  7. 1 class Edit < View
  8. 1 include Deps["aspects.models.palette_optioner"]
  9. 1 expose :model
  10. 5 expose(:palette_options) { |model:| palette_optioner.call model }
  11. 1 expose :fields, default: Core::EMPTY_HASH
  12. 1 expose :errors, default: Core::EMPTY_HASH
  13. end
  14. end
  15. end
  16. end

app/views/models/index.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Models
  5. # The index view.
  6. 1 class Index < Hanami::View
  7. 1 expose :models
  8. 1 expose :query
  9. end
  10. end
  11. end
  12. end

app/views/models/new.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 module Terminus
  4. 1 module Views
  5. 1 module Models
  6. # The new view.
  7. 1 class New < View
  8. 1 include Deps["aspects.models.palette_optioner"]
  9. 1 expose :model
  10. 8 expose(:palette_options) { |model: nil| palette_optioner.call model }
  11. 1 expose :fields, default: Core::EMPTY_HASH
  12. 1 expose :errors, default: Core::EMPTY_HASH
  13. end
  14. end
  15. end
  16. end

app/views/models/show.rb

100.0% lines covered

100.0% branches covered

5 relevant lines. 5 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Models
  5. # The show view.
  6. 1 class Show < View
  7. 1 expose :model
  8. end
  9. end
  10. end
  11. end

app/views/parts/device.rb

100.0% lines covered

100.0% branches covered

42 relevant lines. 42 lines covered and 0 lines missed.
26 total branches, 26 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "hanami/view"
  3. 1 require "refinements/struct"
  4. 1 module Terminus
  5. 1 module Views
  6. 1 module Parts
  7. # The device presenter.
  8. 1 class Device < Hanami::View::Part
  9. 1 include Deps["aspects.screens.fetcher", "aspects.screens.placeholder"]
  10. 1 using Refinements::Struct
  11. 1 def battery_percentage
  12. 48 then: 1 else: 47 battery_charge.positive? ? battery_charge : battery_voltage_to_percent
  13. end
  14. 7 then: 5 else: 1 def wake_description = String(wake_reason).empty? ? "Unknown." : wake_reason
  15. 1 def wifi_percentage
  16. 46 when: 9 case wifi
  17. 9 when: 2 when 0 then 0
  18. 2 when: 1 when ..-91 then 10
  19. 1 when: 1 when -90..-81 then 20
  20. 1 when: 1 when -80..-71 then 30
  21. 1 when: 1 when -70..-67 then 40
  22. 1 when: 1 when -66..-62 then 50
  23. 1 when: 1 when -61..-57 then 60
  24. 1 when: 1 when -56..-52 then 70
  25. 1 when: 27 when -51..-47 then 80
  26. 27 else: 1 when -46..-40 then 90
  27. 1 else 100
  28. end
  29. end
  30. 1 def dimensions = "#{width}x#{height}"
  31. 1 def current_screen
  32. 24 fetcher.call(value).either -> screen { screen },
  33. 14 proc { placeholder.with id: id }
  34. end
  35. 1 private
  36. 1 def battery_voltage_to_percent
  37. 47 when: 9 case battery_voltage
  38. 9 when: 2 when 0 then 0
  39. 2 when: 1 when ..0.45 then 10
  40. 1 when: 1 when 0.46..0.9 then 20
  41. 1 when: 1 when 1.0..1.35 then 30
  42. 1 when: 1 when 1.36..1.8 then 40
  43. 1 when: 1 when 1.81..2.25 then 50
  44. 1 when: 27 when 2.26..2.7 then 60
  45. 27 when: 1 when 2.71..3.15 then 70
  46. 1 when: 1 when 3.16..3.6 then 80
  47. 1 else: 2 when 3.61..4.05 then 90
  48. 2 else 100
  49. end
  50. end
  51. end
  52. end
  53. end
  54. end

app/views/parts/exchange.rb

100.0% lines covered

100.0% branches covered

24 relevant lines. 24 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 require "hanami/view"
  4. 1 require "initable"
  5. 1 require "refinements/string"
  6. 1 module Terminus
  7. 1 module Views
  8. 1 module Parts
  9. # The extension exchange presenter.
  10. 1 class Exchange < Hanami::View::Part
  11. 1 include Deps["aspects.extensions.curler", "aspects.extensions.uri_builder"]
  12. 1 include Initable[json_formatter: Aspects::JSONFormatter]
  13. 1 using Refinements::String
  14. 1 def curl(extension) = curler.call extension, value
  15. 1 def formatted_body = json_formatter.call body
  16. 1 def formatted_data = json_formatter.call data
  17. 1 def formatted_errors = json_formatter.call errors
  18. 1 def formatted_headers = json_formatter.call headers
  19. 1 def formatted_verb = verb.upcase
  20. 1 def requests extension, length = 50
  21. 10 uri_builder.call(extension, template).map { it.trim_end length }
  22. end
  23. 1 def status
  24. 9 span = helpers.tag.method :span
  25. 9 then: 7 if errors.empty?
  26. 7 span.call "Success", class: "bit-pill bit-pill-active"
  27. else: 2 else
  28. 2 span.call "Failure", class: "bit-pill bit-pill-alert"
  29. end
  30. end
  31. end
  32. end
  33. end
  34. end

app/views/parts/extension.rb

100.0% lines covered

100.0% branches covered

17 relevant lines. 17 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/core"
  3. 1 require "hanami/view"
  4. 1 require "initable"
  5. 1 module Terminus
  6. 1 module Views
  7. 1 module Parts
  8. # The extension presenter.
  9. 1 class Extension < Hanami::View::Part
  10. 1 include Initable[json_formatter: Aspects::JSONFormatter]
  11. 1 def alpine_tags
  12. 15 Array(tags).map { %('#{it}') }
  13. .join(",")
  14. 12 .then { "[#{it}]" }
  15. end
  16. 1 def formatted_data = json_formatter.call data
  17. 4 then: 2 else: 1 def formatted_days = days ? days.join(",") : ""
  18. 1 def formatted_fields = json_formatter.call fields
  19. 1 def formatted_start_at
  20. 2 then: 1 else: 1 start_at ? start_at.strftime("%Y-%m-%dT%H:%M:%S") : "2025-01-01T00:00:00"
  21. end
  22. 1 def formatted_static_body = json_formatter.call static_body
  23. end
  24. end
  25. end
  26. end

app/views/parts/firmware.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "hanami/view"
  3. 1 module Terminus
  4. 1 module Views
  5. 1 module Parts
  6. # The firmware presenter.
  7. 1 class Firmware < Hanami::View::Part
  8. 1 def kind_label
  9. 11 when: 1 case kind
  10. 1 else: 10 when "trmnl" then kind.upcase
  11. 10 else kind.capitalize
  12. end
  13. end
  14. end
  15. end
  16. end
  17. end

app/views/parts/ip_address.rb

100.0% lines covered

100.0% branches covered

8 relevant lines. 8 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "hanami/view"
  3. 1 module Terminus
  4. 1 module Views
  5. 1 module Parts
  6. # The dashboard presenter.
  7. 1 class IPAddress < Hanami::View::Part
  8. 1 def address = addr.ip_address
  9. 1 def address_with_kind = "#{address} (#{kind})"
  10. 59 then: 2 else: 56 def kind = name == "en0" ? :wireless : :wired
  11. end
  12. end
  13. end
  14. end

app/views/parts/model.rb

100.0% lines covered

100.0% branches covered

21 relevant lines. 21 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "hanami/view"
  3. 1 require "initable"
  4. 1 require "refinements/array"
  5. 1 module Terminus
  6. 1 module Views
  7. 1 module Parts
  8. # The model presenter.
  9. 1 class Model < Hanami::View::Part
  10. 1 include Deps[
  11. join_repository: "repositories.model_palette",
  12. palette_repository: "repositories.palette"
  13. ]
  14. 1 include Initable[json_formatter: Aspects::JSONFormatter]
  15. 1 using Refinements::Array
  16. 1 def allowed_palettes
  17. 5 join_repository.where(model_id: id)
  18. .map(&:palette_id)
  19. 5 .then { |ids| palette_repository.where(id: ids) }
  20. .map(&:label)
  21. 5 then: 4 else: 1 .then { it.empty? ? ["All"] : it }
  22. .to_sentence
  23. end
  24. 6 then: 1 else: 4 def default_palette_label = default_palette_id ? default_palette.label : "None"
  25. 1 def dimensions = "#{width}x#{height}"
  26. 1 def formatted_css = json_formatter.call css
  27. 1 def kind_label
  28. 17 when: 2 case kind
  29. 2 else: 15 when "byod", "trmnl" then kind.upcase
  30. 15 else kind.capitalize
  31. end
  32. end
  33. end
  34. end
  35. end
  36. end

app/views/parts/playlist.rb

100.0% lines covered

100.0% branches covered

13 relevant lines. 13 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "hanami/view"
  3. 1 module Terminus
  4. 1 module Views
  5. 1 module Parts
  6. # The playlist presenter.
  7. 1 class Playlist < Hanami::View::Part
  8. 1 include Deps["aspects.screens.placeholder"]
  9. 1 def current_screen_pill item, label = "Current Screen"
  10. 3 else: 2 then: 1 return unless current_item_id == item.id
  11. 2 helpers.tag.div label, class: "bit-pill bit-pill-active"
  12. end
  13. # :reek:ManualDispatch
  14. 1 def current_screen
  15. 15 then: 3 if current_item_id && respond_to?(:current_item)
  16. 3 current_item.screen
  17. else: 12 else
  18. 12 placeholder.with id:, uri: "blank.svg"
  19. end
  20. end
  21. end
  22. end
  23. end
  24. end

app/views/parts/screen.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "hanami/view"
  3. 1 module Terminus
  4. 1 module Views
  5. 1 module Parts
  6. # The screen presenter.
  7. 1 class Screen < Hanami::View::Part
  8. 14 then: 9 else: 4 def dimensions = width && height ? "#{width}x#{height}" : "Unknown"
  9. 3 then: 1 else: 1 def type = mime_type ? mime_type.delete_prefix("image/").upcase : "Unknown"
  10. end
  11. end
  12. end
  13. end

app/views/parts/user.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "hanami/view"
  3. 1 module Terminus
  4. 1 module Views
  5. 1 module Parts
  6. # The user presenter.
  7. 1 class User < Hanami::View::Part
  8. 1 attr_accessor :password
  9. 1 def pill
  10. 14 when: 5 case status_id
  11. 5 when: 7 when 1 then "caution"
  12. 7 when: 1 when 2 then "active"
  13. 1 else: 1 when 3 then "inactive"
  14. 1 else "unknown"
  15. end
  16. end
  17. end
  18. end
  19. end
  20. end

app/views/playlists/clone/new.rb

100.0% lines covered

100.0% branches covered

5 relevant lines. 5 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Playlists
  5. 1 module Clone
  6. # The new view.
  7. 1 class New < Views::Playlists::New
  8. end
  9. end
  10. end
  11. end
  12. end

app/views/playlists/edit.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 module Terminus
  4. 1 module Views
  5. 1 module Playlists
  6. # The edit view.
  7. 1 class Edit < View
  8. 1 expose :playlist
  9. 1 expose :items, default: Core::EMPTY_ARRAY
  10. 1 expose :fields, default: Core::EMPTY_HASH
  11. 1 expose :errors, default: Core::EMPTY_HASH
  12. end
  13. end
  14. end
  15. end

app/views/playlists/index.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Playlists
  5. # The index view.
  6. 1 class Index < View
  7. 1 expose :playlists
  8. 1 expose :query
  9. end
  10. end
  11. end
  12. end

app/views/playlists/items/index.rb

100.0% lines covered

100.0% branches covered

8 relevant lines. 8 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Playlists
  5. 1 module Items
  6. # The index view.
  7. 1 class Index < View
  8. 1 expose :playlist_id
  9. 1 expose :items
  10. 1 expose :query
  11. end
  12. end
  13. end
  14. end
  15. end

app/views/playlists/items/new.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 module Terminus
  4. 1 module Views
  5. 1 module Playlists
  6. 1 module Items
  7. # The new view.
  8. 1 class New < View
  9. 1 expose :playlist
  10. 1 expose :screen_options, decorate: false
  11. 1 expose :item
  12. 1 expose :fields, default: Core::EMPTY_HASH
  13. 1 expose :errors, default: Core::EMPTY_HASH
  14. end
  15. end
  16. end
  17. end
  18. end

app/views/playlists/items/show.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Playlists
  5. 1 module Items
  6. # The show view.
  7. 1 class Show < View
  8. 1 expose :item
  9. end
  10. end
  11. end
  12. end
  13. end

app/views/playlists/mirror/edit.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Playlists
  5. 1 module Mirror
  6. # The edit view.
  7. 1 class Edit < View
  8. 1 expose :playlist
  9. 1 expose :devices
  10. end
  11. end
  12. end
  13. end
  14. end

app/views/playlists/new.rb

100.0% lines covered

100.0% branches covered

8 relevant lines. 8 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 module Terminus
  4. 1 module Views
  5. 1 module Playlists
  6. # The new view.
  7. 1 class New < View
  8. 1 expose :playlist
  9. 1 expose :fields, default: Core::EMPTY_HASH
  10. 1 expose :errors, default: Core::EMPTY_HASH
  11. end
  12. end
  13. end
  14. end

app/views/playlists/screens/show.rb

100.0% lines covered

100.0% branches covered

26 relevant lines. 26 lines covered and 0 lines missed.
10 total branches, 10 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Playlists
  5. 1 module Screens
  6. # The show view.
  7. 1 class Show < View
  8. 1 include Deps[:routes]
  9. 1 expose :playlist
  10. 1 expose :current
  11. 12 expose(:index, decorate: false) { |screens, current:| screens.index current }
  12. 12 expose(:total, decorate: false) { |screens| screens.size - 1 }
  13. 1 expose :status, decorate: false do |index, total|
  14. 11 then: 10 else: 1 "#{index + 1} of #{total + 1}" if index && total
  15. end
  16. 1 expose :previous_uri, decorate: false do |playlist:, before:|
  17. 11 then: 10 else: 1 routes.path :playlist_screen, playlist_id: playlist.id, id: before.id if before
  18. end
  19. 1 expose :next_uri, decorate: false do |playlist:, after:|
  20. 11 then: 10 else: 1 routes.path :playlist_screen, playlist_id: playlist.id, id: after.id if after
  21. end
  22. 1 expose :first_uri, decorate: false do |screens, playlist:|
  23. 11 first = screens.first
  24. 11 then: 10 else: 1 routes.path :playlist_screen, playlist_id: playlist.id, id: first.id if first
  25. end
  26. 1 expose :last_uri, decorate: false do |screens, playlist:|
  27. 11 last = screens.last
  28. 11 then: 10 else: 1 routes.path :playlist_screen, playlist_id: playlist.id, id: last.id if last
  29. end
  30. 1 private_expose :routes
  31. 1 private_expose :before, decorate: false
  32. 1 private_expose :after, decorate: false
  33. 12 private_expose(:screens, decorate: false) { |playlist:| playlist.screens }
  34. end
  35. end
  36. end
  37. end
  38. end

app/views/playlists/show.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Playlists
  5. # The show view.
  6. 1 class Show < View
  7. 1 expose :playlist
  8. 1 expose :items
  9. end
  10. end
  11. end
  12. end

app/views/problem_details/index.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module ProblemDetails
  5. # The index view.
  6. 1 class Index < View
  7. end
  8. end
  9. end
  10. end

app/views/scopes/form_field.rb

100.0% lines covered

100.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
8 total branches, 8 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 require "refinements/array"
  4. 1 module Terminus
  5. 1 module Views
  6. 1 module Scopes
  7. # Groups form label and input together as a single form field.
  8. 1 class FormField < Hanami::View::Scope
  9. 1 using Refinements::Array
  10. 1 def alpine
  11. 619 else: 126 then: 493 return unless locals.key? :alpine
  12. 253 locals[:alpine].transform_keys! { "x-#{it}" }
  13. 127 .map { |key, value| %(#{key}="#{value}") }
  14. .join(" ")
  15. 126 .then { %( #{it}) }
  16. end
  17. 1 def toggle_error kind = "form-field"
  18. 621 then: 31 else: 590 errors.fetch(key, Core::EMPTY_ARRAY).any? ? [kind, "error"].compact.join(" ") : kind
  19. end
  20. 1 def error_message
  21. 649 else: 648 then: 1 return Core::EMPTY_STRING unless locals.key? :errors
  22. 648 else: 59 then: 589 return Core::EMPTY_STRING unless errors.key? key
  23. 59 errors[key].to_sentence
  24. end
  25. 1 def render(path = "shared/form_field") = super
  26. end
  27. end
  28. end
  29. end

app/views/scopes/popover_default_content.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Scopes
  5. # Provides customized popover content.
  6. 1 class PopoverDefaultContent < Hanami::View::Scope
  7. 1 def dom_id = "popover-#{name}"
  8. 1 def render(path = "shared/popovers/content/default") = super
  9. end
  10. end
  11. end
  12. end

app/views/scopes/popover_screen_content.rb

100.0% lines covered

100.0% branches covered

8 relevant lines. 8 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Scopes
  5. # Provides customized popover content.
  6. 1 class PopoverScreenContent < Hanami::View::Scope
  7. 1 def dom_id = "popover-screen-#{id}"
  8. 1 def width = locals.fetch __method__, 800
  9. 1 def height = locals.fetch __method__, 480
  10. 1 def render(path = "shared/popovers/content/screen") = super
  11. end
  12. end
  13. end
  14. end

app/views/screens/edit.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 module Terminus
  4. 1 module Views
  5. 1 module Screens
  6. # The edit view.
  7. 1 class Edit < View
  8. 1 expose :models
  9. 1 expose :screen
  10. 1 expose :fields, default: Core::EMPTY_HASH
  11. 1 expose :errors, default: Core::EMPTY_HASH
  12. end
  13. end
  14. end
  15. end

app/views/screens/gaffe/new.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Screens
  5. 1 module Gaffe
  6. # The new view.
  7. 1 class New < View
  8. 1 config.layout = "gaffe"
  9. 1 expose :message, decorate: false
  10. end
  11. end
  12. end
  13. end
  14. end

app/views/screens/index.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Screens
  5. # The index view.
  6. 1 class Index < Hanami::View
  7. 1 expose :screens
  8. 1 expose :query
  9. end
  10. end
  11. end
  12. end

app/views/screens/new.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 module Terminus
  4. 1 module Views
  5. 1 module Screens
  6. # The new view.
  7. 1 class New < View
  8. 1 expose :models
  9. 1 expose :screen
  10. 1 expose :fields, default: Core::EMPTY_HASH
  11. 1 expose :errors, default: Core::EMPTY_HASH
  12. end
  13. end
  14. end
  15. end

app/views/screens/show.rb

100.0% lines covered

100.0% branches covered

5 relevant lines. 5 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Screens
  5. # The show view.
  6. 1 class Show < View
  7. 1 expose :screen
  8. end
  9. end
  10. end
  11. end

app/views/screens/sleep/new.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Screens
  5. 1 module Sleep
  6. # The new view.
  7. 1 class New < View
  8. 1 config.layout = "sleep"
  9. end
  10. end
  11. end
  12. end
  13. end

app/views/screens/welcome/new.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Screens
  5. 1 module Welcome
  6. # The new view.
  7. 1 class New < View
  8. 1 config.layout = "welcome"
  9. 1 expose :device
  10. end
  11. end
  12. end
  13. end
  14. end

app/views/users/edit.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 module Terminus
  4. 1 module Views
  5. 1 module Users
  6. # The edit view.
  7. 1 class Edit < View
  8. 1 expose :user
  9. 1 expose :statuses
  10. 1 expose :fields, default: Core::EMPTY_HASH
  11. 1 expose :errors, default: Core::EMPTY_HASH
  12. end
  13. end
  14. end
  15. end

app/views/users/index.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Users
  5. # The index view.
  6. 1 class Index < Hanami::View
  7. 1 expose :users
  8. 1 expose :query
  9. end
  10. end
  11. end
  12. end

app/views/users/new.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 module Terminus
  4. 1 module Views
  5. 1 module Users
  6. # The new view.
  7. 1 class New < View
  8. 1 expose :user
  9. 1 expose :statuses
  10. 1 expose :fields, default: Core::EMPTY_HASH
  11. 1 expose :errors, default: Core::EMPTY_HASH
  12. end
  13. end
  14. end
  15. end

app/views/users/show.rb

100.0% lines covered

100.0% branches covered

5 relevant lines. 5 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Views
  4. 1 module Users
  5. # The show view.
  6. 1 class Show < View
  7. 1 expose :user
  8. end
  9. end
  10. end
  11. end

config/providers/htmx.rb

100.0% lines covered

100.0% branches covered

8 relevant lines. 8 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 Hanami.app.register_provider :htmx do
  3. 2 prepare { require "htmx" }
  4. 1 start do
  5. 1 toggler = lambda do |request, default = "app"|
  6. 106 then: 67 else: 39 HTMX.request?(request.env, :request, "true") ? false : default
  7. end
  8. 1 register :htmx, HTMX
  9. 1 register :htmx_defaults, {"allowScriptTags" => false, "defaultSwapStyle" => "outerHTML"}.freeze
  10. 1 register :htmx_layout, toggler
  11. end
  12. end

config/providers/http.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 Hanami.app.register_provider :http do
  3. 1 prepare do
  4. 1 require "connection_pool"
  5. 1 require "http"
  6. end
  7. 1 start do
  8. 1 slice.start :logger
  9. 1 connect, read, write = slice[:settings].to_h.values_at :http_timeout_connect,
  10. :http_timeout_read,
  11. :http_timeout_write
  12. 1 http = ConnectionPool::Wrapper.new size: ENV.fetch("HANAMI_MAX_THREADS", 5) do
  13. 1 HTTP.timeout(connect:, read:, write:)
  14. .use(:auto_inflate)
  15. .use(logging: {logger: slice[:logger]})
  16. .headers("User-Agent" => "http.rb/#{HTTP::VERSION} (#{Hanami.app.app_name})")
  17. end
  18. 1 register :http, http
  19. end
  20. 1 stop { slice[:http].close }
  21. end

config/providers/liquid.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 Hanami.app.register_provider :liquid, namespace: true do
  3. 2 prepare { require "trmnl/liquid" }
  4. 1 start do
  5. 2 default = TRMNL::Liquid.new { |environment| environment.error_mode = :strict }
  6. 1 basic = lambda do |template, data, environment: default|
  7. 22 Liquid::Template.parse(template, environment:).render(data)
  8. end
  9. 1 sanitize = lambda do |template, data, environment: default|
  10. 19 slice["aspects.sanitizer"].call Liquid::Template.parse(template, environment:).render(data)
  11. end
  12. 1 register :basic, basic
  13. 1 register :sanitize, sanitize
  14. end
  15. end

config/providers/logger.rb

100.0% lines covered

100.0% branches covered

2 relevant lines. 2 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "../../app/providers/logger"
  3. 1 Hanami.app.register_provider :logger, source: Terminus::Providers::Logger

config/providers/mini_magick.rb

100.0% lines covered

100.0% branches covered

13 relevant lines. 13 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "refinements/pathname"
  3. 1 using Refinements::Pathname
  4. 1 Hanami.app.register_provider :mini_magick, namespace: true do
  5. 2 prepare { require "mini_magick" }
  6. 1 start do
  7. 1 MiniMagick.configure do |config|
  8. 1 config.errors = true
  9. 1 config.warnings = true
  10. 1 config.restricted_env = true
  11. 1 config.tmpdir = slice.root.join("tmp/mini_magick").make_ancestors.make_dir
  12. 1 config.logger = slice[:logger]
  13. end
  14. 1 register :core, MiniMagick
  15. 1 register :image, MiniMagick::Image
  16. end
  17. end

config/providers/shrine.rb

100.0% lines covered

100.0% branches covered

15 relevant lines. 15 lines covered and 0 lines missed.
1 total branches, 1 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 Hanami.app.register_provider :shrine do
  3. 1 prepare do
  4. 1 require "shrine"
  5. 1 require "shrine/storage/file_system"
  6. end
  7. 1 start do
  8. 1 then: 1 Shrine.storages = if Hanami.env? :test
  9. 1 {cache: Shrine::Storage::Memory.new, store: Shrine::Storage::Memory.new}
  10. else
  11. {
  12. skipped # :nocov:
  13. skipped cache: Shrine::Storage::FileSystem.new("public", prefix: "uploads/cache"),
  14. skipped store: Shrine::Storage::FileSystem.new("public", prefix: "uploads")
  15. skipped # :nocov:
  16. }
  17. end
  18. 1 Shrine.plugin :add_metadata
  19. 1 Shrine.plugin :determine_mime_type, analyzer: :marcel
  20. 1 Shrine.plugin :entity
  21. 1 Shrine.plugin :signature
  22. 48 Shrine.plugin :store_dimensions, analyzer: :mini_magick, on_error: proc { "Omit" }
  23. 1 Shrine.plugin :validation_helpers
  24. 1 Shrine.logger = slice[:logger]
  25. 1 register :shrine, Shrine
  26. end
  27. end

config/providers/sidekiq.rb

100.0% lines covered

100.0% branches covered

2 relevant lines. 2 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "../../app/providers/logger"
  3. 1 Hanami.app.register_provider :sidekiq, source: Terminus::Providers::Sidekiq

config/providers/trmnl_api.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 Hanami.app.register_provider :trmnl_api do
  3. 2 prepare { require "trmnl/api" }
  4. 1 start do
  5. 1 slice.start :http
  6. 1 TRMNL::API::Container.register :http, slice[:http]
  7. 1 TRMNL::API::Container.register :logger, slice[:logger]
  8. 2 recipes = TRMNL::API.new { |settings| settings.uri = "https://trmnl.com" }
  9. 1 register :trmnl_api, TRMNL::API.new
  10. 1 register :trmnl_api_recipes, recipes
  11. end
  12. end

config/routes.rb

100.0% lines covered

100.0% branches covered

124 relevant lines. 124 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "sidekiq/web"
  3. 1 require "sidekiq-scheduler/web"
  4. 1 require_relative "../app/aspects/screens/designer/middleware"
  5. 1 module Terminus
  6. # The application base routes.
  7. # rubocop:todo Metrics/ClassLength
  8. 1 class Routes < Hanami::Routes
  9. 2 slice(:authentication, at: "/") { use Authentication::Middleware }
  10. 1 mount Sidekiq::Web, at: "/sidekiq"
  11. 1 get "/", to: "dashboard.show", as: :root
  12. # rubocop:todo Metrics/BlockLength
  13. 1 scope "api" do
  14. 1 get "/devices", to: "api.devices.index", as: :devices
  15. 1 get "/devices/:id", to: "api.devices.show", as: :device
  16. 1 post "/devices", to: "api.devices.create", as: :device_create
  17. 1 patch "/devices/:id", to: "api.devices.patch", as: :device_patch
  18. 1 delete "/devices/:id", to: "api.devices.delete", as: :device_delete
  19. 1 resource :display, to: "api.display", only: :show
  20. 1 get "/firmware", to: "api.firmware.index", as: :firmware
  21. 1 get "/firmware/:id", to: "api.firmware.show", as: :firmware_show
  22. 1 post "/firmware", to: "api.firmware.create", as: :firmware_create
  23. 1 patch "/firmware/:id", to: "api.firmware.patch", as: :firmware_patch
  24. 1 delete "/firmware/:id", to: "api.firmware.delete", as: :firmware_delete
  25. 1 resource :log, to: "api.log", only: :create
  26. 1 get "/models", to: "api.models.index", as: :models
  27. 1 get "/models/:id", to: "api.models.show", as: :model
  28. 1 post "/models", to: "api.models.create", as: :model_create
  29. 1 patch "/models/:id", to: "api.models.patch", as: :model_patch
  30. 1 delete "/models/:id", to: "api.models.delete", as: :model_delete
  31. 1 get "/playlists", to: "api.playlists.index", as: :playlists
  32. 1 get "/playlists/:id", to: "api.playlists.show", as: :playlist
  33. 1 post "/playlists", to: "api.playlists.create", as: :playlist_create
  34. 1 patch "/playlists/:id", to: "api.playlists.patch", as: :playlist_patch
  35. 1 delete "/playlists/:id", to: "api.playlists.delete", as: :playlist_delete
  36. 1 get "/screens", to: "api.screens.index", as: :screens
  37. 1 post "/screens", to: "api.screens.create", as: :screen_create
  38. 1 patch "/screens/:id", to: "api.screens.patch", as: :screen_patch
  39. 1 delete "/screens/:id", to: "api.screens.delete", as: :screen_delete
  40. 1 resource :setup, to: "api.setup", only: :show
  41. end
  42. # rubocop:enable Metrics/BlockLength
  43. 1 scope "bulk" do
  44. 1 delete "/devices/:device_id/logs", to: "bulk.devices.logs.delete", as: :device_logs_delete
  45. 1 delete "/firmware", to: "bulk.firmware.delete", as: :firmware_delete
  46. end
  47. 1 get "/devices", to: "devices.index", as: :devices
  48. 1 get "/devices/:id", to: "devices.show", as: :device
  49. 1 get "/devices/new", to: "devices.new", as: :device_new
  50. 1 post "/devices", to: "devices.create", as: :device_create
  51. 1 get "/devices/:id/edit", to: "devices.edit", as: :device_edit
  52. 1 put "/devices/:id", to: "devices.update", as: :device_update
  53. 1 delete "/devices/:id", to: "devices.delete", as: :device_delete
  54. 1 get "/devices/:device_id/logs", to: "devices.logs.index", as: :device_logs
  55. 1 get "/devices/:device_id/logs/:id", to: "devices.logs.show", as: :device_log
  56. 1 delete "/devices/:device_id/logs/:id", to: "devices.logs.delete", as: :device_log_delete
  57. 1 resource :designer, to: "designer", only: %i[show create]
  58. 1 get "/extensions", to: "extensions.index", as: :extensions
  59. 1 get "/extensions/new", to: "extensions.new", as: :extension_new
  60. 1 post "/extensions", to: "extensions.create", as: :extension_create
  61. 1 get "/extensions/:id/edit", to: "extensions.edit", as: :extension_edit
  62. 1 put "/extensions/:id", to: "extensions.update", as: :extension_update
  63. 1 delete "/extensions/:id", to: "extensions.delete", as: :extension_delete
  64. 1 get "/extensions/gallery", to: "extensions.gallery.index", as: :extensions_gallery
  65. 1 post "/extensions/gallery", to: "extensions.gallery.create", as: :extensions_gallery_create
  66. 1 post "/extensions/:extension_id/build",
  67. to: "extensions.build.create",
  68. as: :extension_build_create
  69. 1 get "/extensions/:extension_id/clone/new", to: "extensions.clone.new", as: :extension_clone_new
  70. 1 post "/extensions/:extension_id/clone",
  71. to: "extensions.clone.create",
  72. as: :extension_clone_create
  73. 1 get "/extensions/:extension_id/exchanges",
  74. to: "extensions.exchanges.index",
  75. as: :extension_exchanges
  76. 1 get "/extensions/:extension_id/exchanges/new",
  77. to: "extensions.exchanges.new",
  78. as: :extension_exchange_new
  79. 1 post "/extensions/:extension_id/exchanges",
  80. to: "extensions.exchanges.create",
  81. as: :extension_exchanges
  82. 1 get "/extensions/:extension_id/exchanges/:id/edit",
  83. to: "extensions.exchanges.edit",
  84. as: :extension_exchange_edit
  85. 1 put "/extensions/:extension_id/exchanges/:id",
  86. to: "extensions.exchanges.update",
  87. as: :extension_exchange
  88. 1 delete "/extensions/:extension_id/exchanges/:id",
  89. to: "extensions.exchanges.delete",
  90. as: :extension_exchange
  91. 1 get "/extensions/:extension_id/export", to: "extensions.export.show", as: :extension_export
  92. 1 get "/extensions/:extension_id/preview", to: "extensions.preview.show", as: :extension_preview
  93. 1 get "/extensions/:extension_id/sources", to: "extensions.sources.index", as: :extension_sources
  94. 1 get "/extensions/:extension_id/sensors", to: "extensions.sensors.index", as: :extension_sensors
  95. 1 get "/firmware", to: "firmware.index", as: :firmware
  96. 1 get "/firmware/:id", to: "firmware.show", as: :firmware_show
  97. 1 get "/firmware/new", to: "firmware.new", as: :firmware_new
  98. 1 post "/firmware", to: "firmware.create", as: :firmware_create
  99. 1 get "/firmware/:id/edit", to: "firmware.edit", as: :firmware_edit
  100. 1 put "/firmware/:id", to: "firmware.update", as: :firmware_update
  101. 1 delete "/firmware/:id", to: "firmware.delete", as: :firmware_delete
  102. 1 get "/models", to: "models.index", as: :models
  103. 1 get "/models/:id", to: "models.show", as: :model
  104. 1 get "/models/new", to: "models.new", as: :model_new
  105. 1 post "/models", to: "models.create", as: :model_create
  106. 1 get "/models/:id/edit", to: "models.edit", as: :model_edit
  107. 1 put "/models/:id", to: "models.update", as: :model_update
  108. 1 delete "/models/:id", to: "models.delete", as: :model_delete
  109. 1 get "/models/:model_id/clone/new", to: "models.clone.new", as: :model_clone_new
  110. 1 post "/models/:model_id/clone", to: "models.clone.create", as: :model_clone_create
  111. 1 get "/playlists", to: "playlists.index", as: :playlists
  112. 1 get "/playlists/:id", to: "playlists.show", as: :playlist
  113. 1 get "/playlists/new", to: "playlists.new", as: :playlist_new
  114. 1 post "/playlists", to: "playlists.create", as: :playlist_create
  115. 1 get "/playlists/:id/edit", to: "playlists.edit", as: :playlist_edit
  116. 1 put "/playlists/:id", to: "playlists.update", as: :playlist_update
  117. 1 delete "/playlists/:id", to: "playlists.delete", as: :playlist_delete
  118. 1 get "/playlists/:playlist_id/clone/new", to: "playlists.clone.new", as: :playlist_clone_new
  119. 1 post "/playlists/:playlist_id/clone", to: "playlists.clone.create", as: :playlist_clone_create
  120. 1 get "/playlists/:playlist_id/items", to: "playlists.items.index", as: :playlist_items
  121. 1 get "/playlists/:playlist_id/items/:id", to: "playlists.items.show", as: :playlist_item
  122. 1 get "/playlists/:playlist_id/items/new", to: "playlists.items.new", as: :playlist_item_new
  123. 1 post "/playlists/:playlist_id/items", to: "playlists.items.create", as: :playlist_item_create
  124. 1 get "/playlists/:playlist_id/items/:id/edit",
  125. to: "playlists.items.edit",
  126. as: :playlist_item_edit
  127. 1 put "/playlists/:playlist_id/items/:id", to: "playlists.items.update", as: :playlist_item_update
  128. 1 delete "/playlists/:playlist_id/items/:id",
  129. to: "playlists.items.delete",
  130. as: :playlist_item_delete
  131. 1 get "/playlists/:playlist_id/mirror/edit",
  132. to: "playlists.mirror.edit",
  133. as: :playlist_mirror_edit
  134. 1 put "/playlists/:playlist_id/mirror", to: "playlists.mirror.update", as: :playlist_mirror_update
  135. 1 get "/playlists/:playlist_id/screens", to: "playlists.screens.index", as: :playlist_screens
  136. 1 get "/playlists/:playlist_id/screens/:id", to: "playlists.screens.show", as: :playlist_screen
  137. 1 resources :problem_details, to: "problem_details", only: :index, as: :problem_details
  138. 1 get "/screens", to: "screens.index", as: :screens
  139. 1 get "/screens/:id", to: "screens.show", as: :screen
  140. 1 get "/screens/new", to: "screens.new", as: :screen_new
  141. 1 post "/screens", to: "screens.create", as: :screen_create
  142. 1 get "/screens/:id/edit", to: "screens.edit", as: :screen_edit
  143. 1 put "/screens/:id", to: "screens.update", as: :screen_update
  144. 1 delete "/screens/:id", to: "screens.delete", as: :screen_delete
  145. 1 get "/users", to: "users.index", as: :users
  146. 1 get "/users/:id", to: "users.show", as: :user
  147. 1 get "/users/new", to: "users.new", as: :user_new
  148. 1 post "/users", to: "users.create", as: :user_create
  149. 1 get "/users/:id/edit", to: "users.edit", as: :user_edit
  150. 1 put "/users/:id", to: "users.update", as: :user_update
  151. 2 slice(:health, at: "/up") { root to: "show" }
  152. 1 use Rack::Static, root: "public", urls: ["/.well-known/security.txt", "/fonts", "/uploads"]
  153. 1 use Aspects::Screens::Designer::Middleware, pattern: %r(/preview/(?<name>.+))
  154. end
  155. # rubocop:enable Metrics/ClassLength
  156. end

lib/terminus/ip_finder.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "initable"
  3. 1 require "socket"
  4. 1 module Terminus
  5. # Finds wired and wireless addresses.
  6. 1 class IPFinder
  7. 1 include Initable[socket: Socket]
  8. 1 def all pattern: /\A(en|wl|eth)/
  9. 67 socket.getifaddrs.select do |interface|
  10. 232 address = interface.addr
  11. 232 interface.name.match?(pattern) && address.ipv4? && !address.ipv4_loopback?
  12. end
  13. end
  14. 1 def wired pattern: /\A(en[1-9]|eth)/
  15. 10 all.find { |address| address.name.match? pattern }
  16. 5 then: 3 else: 2 .then { it.addr.ip_address if it }
  17. end
  18. end
  19. end

lib/terminus/refines/actions/response.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Terminus
  3. 1 module Refines
  4. 1 module Actions
  5. # Modifies and enhances default Hanami action response behavior.
  6. 1 module Response
  7. 1 refine Hanami::Action::Response do
  8. 1 def with body:, format: nil, status: 200
  9. 50 @body = [body]
  10. 50 @status = status
  11. 50 then: 39 else: 11 self.format = format if format
  12. 50 self
  13. end
  14. end
  15. end
  16. end
  17. end
  18. end

slices/authentication/feature.rb

100.0% lines covered

100.0% branches covered

21 relevant lines. 21 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 require "rodauth"
  4. 1 Rodauth::Feature.define :hanami do
  5. 1 auth_value_method :hanami_view, nil
  6. 1 def view(name, *)
  7. 82 layout_path = view_base.class.layout_path view_base.config.layout
  8. 82 scope = view_rendering.scope rodauth: self
  9. 164 view_rendering.template(layout_path, scope) { render name }
  10. end
  11. 1 def render name
  12. 246 else: 82 then: 164 return super unless view_template? name
  13. 82 view_rendering.template name, view_rendering.scope(rodauth: self)
  14. end
  15. 1 private
  16. 1 def view_template? name
  17. # rubocop:todo Style/SendWithLiteralMethodName
  18. 246 view_rendering.renderer.__send__ :lookup, name, view_base.config.default_format
  19. # rubocop:enable Style/SendWithLiteralMethodName
  20. end
  21. 1 def view_rendering
  22. 574 @view_rendering ||= view_base.rendering format: view_base.config.default_format,
  23. context: view_context
  24. end
  25. 1 def view_context
  26. 82 @view_context ||= begin
  27. 82 action_request = Hanami::Action::Request.new(
  28. env: request.env,
  29. params: request.params,
  30. session_enabled: true
  31. )
  32. 82 view_base.config.default_context.class.new request: action_request
  33. end
  34. end
  35. 1 def view_base
  36. 656 @view_base ||= hanami_view.call
  37. end
  38. end

slices/authentication/middleware.rb

100.0% lines covered

100.0% branches covered

74 relevant lines. 74 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 require "roda"
  4. 1 require "rodauth"
  5. 1 require_relative "feature"
  6. 1 module Authentication
  7. # Specialized Roda middleware for authentication.
  8. 1 class Middleware < Roda
  9. 1 UNVERIFIED_ID = 1
  10. 1 VERIFIED_ID = 2
  11. 1 plugin :middleware
  12. 1 plugin :rodauth, json: true do
  13. 1 enable :active_sessions,
  14. :audit_logging,
  15. :change_login,
  16. :change_password,
  17. :create_account,
  18. :disallow_common_passwords,
  19. :hanami,
  20. :jwt_refresh,
  21. :login,
  22. :logout,
  23. :remember,
  24. :recovery_codes,
  25. :session_expiration
  26. 1 db Authentication::Slice["db.gateway"].connection
  27. # Feature (automatic): base
  28. 1 accounts_table :user
  29. 1 after_login { remember_login }
  30. 1 already_logged_in { redirect "/" }
  31. 1 flash_error_key :alert
  32. 1 hmac_secret Hanami.app[:settings].app_secret
  33. 1 login_label "Email"
  34. 1 password_hash_table :user_password_hash
  35. 1 require_login_error_flash "Please log in to continue."
  36. 1 template_opts layout: nil
  37. 1 unverified_account_message "Unverified user, please verify before logging in."
  38. 1 after_login do
  39. 114 else: 113 then: 1 unless account[:status_id] == VERIFIED_ID
  40. 1 logout
  41. 1 set_redirect_error_flash "Your account requires verification before proceeding. " \
  42. "Please contact administration for access."
  43. 1 redirect "/login"
  44. end
  45. end
  46. # Feature (automatic): login_password_requirements_base
  47. 1 require_password_confirmation? false
  48. # Feature: active_sessions
  49. 1 active_sessions_account_id_column :user_id
  50. 1 active_sessions_table :user_active_session_key
  51. # Feature: audit_logging
  52. 1 audit_logging_table :user_authentication_audit_log
  53. 1 audit_logging_account_id_column :user_id
  54. # Feature: change_login
  55. 1 change_login_route "me/login"
  56. 3 change_login_view { view "login_update", nil }
  57. # Feature: change_password
  58. 1 change_password_route "me/password"
  59. 2 change_password_view { view "password_update", nil }
  60. 1 change_password_button "Save"
  61. # Feature: create_account
  62. 1 create_account_button "Create"
  63. 1 create_account_link_text "Register."
  64. 1 create_account_route "register"
  65. 5 create_account_view { view "register", nil }
  66. 1 change_login_button "Save"
  67. 1 after_create_account do
  68. 3 user_id = account[:id]
  69. 3 then: 2 else: 1 status_id = db[:user].one? ? VERIFIED_ID : UNVERIFIED_ID
  70. 3 account_id = db[:account].insert_conflict(target: :name, update: {name: "default"})
  71. .insert name: "default", label: "Default"
  72. 3 db[:user].where(id: user_id).update(name: param("name"), status_id:)
  73. 3 db[:membership].insert(user_id: user_id, account_id:)
  74. 3 else: 2 then: 1 unless status_id == VERIFIED_ID
  75. 1 logout
  76. 1 set_redirect_error_flash "Your account requires verification before proceeding. " \
  77. "Please contact administration for access."
  78. 1 redirect "/login"
  79. end
  80. end
  81. # Feature (custom): hanami
  82. 83 hanami_view(proc { View.new })
  83. # Feature: jwt
  84. 1 jwt_secret Hanami.app[:settings].app_secret
  85. 1 jwt_refresh_route "api/jwt"
  86. # Feature: jwt_refresh
  87. 1 jwt_access_token_period Hanami.app[:settings].api_access_token_period
  88. 1 jwt_refresh_token_account_id_column :user_id
  89. 1 jwt_refresh_token_table :user_jwt_refresh_key
  90. # Feature: login
  91. 1 login_error_flash "There was an error signing in."
  92. 1 login_form_footer_links_heading { nil }
  93. 1 login_notice_flash "You have been logged in."
  94. 1 login_return_to_requested_location? true
  95. 1 multi_phase_login_view { view "login_multi_phase", nil }
  96. # Feature: logout
  97. 1 logout_notice_flash "You have been logged out."
  98. 1 logout_redirect "/"
  99. # Feature: remember
  100. 1 remember_button "Save"
  101. 1 remember_table :user_remember_key
  102. 1 remember_route "me/remember"
  103. # Feature: recovery_codes
  104. 1 recovery_codes_table :user_recovery_code
  105. # Feature: session_expiration
  106. 1 session_inactivity_timeout Hanami.app[:settings].session_inactivity_limit
  107. 1 max_session_lifetime Hanami.app[:settings].session_lifetime_limit
  108. end
  109. 1 route do |request|
  110. 508 rodauth.check_session_expiration
  111. 508 env["rodauth"] = rodauth
  112. 508 request.rodauth
  113. end
  114. end
  115. end

slices/authentication/view.rb

100.0% lines covered

100.0% branches covered

3 relevant lines. 3 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 module Authentication
  4. # The slice view.
  5. 1 class View < Terminus::View
  6. 1 config.paths += ["app/templates"]
  7. end
  8. end

slices/authentication/views/context.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 module Authentication
  4. 1 module Views
  5. # The slice view context.
  6. 1 class Context < Hanami::View::Context
  7. 1 include Deps[main_assets: "main.assets"]
  8. end
  9. end
  10. end

slices/health/action.rb

100.0% lines covered

100.0% branches covered

2 relevant lines. 2 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 module Health
  4. # The slice base action.
  5. 1 class Action < Terminus::Action
  6. end
  7. end

slices/health/actions/show.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Health
  3. 1 module Actions
  4. # The show action.
  5. 1 class Show < Health::Action
  6. 1 handle_exception Exception => :down
  7. 1 def handle(*, response) = response.render view, color: :green
  8. 1 private
  9. 1 def down(*, response, _exception) = response.render view, color: :red, status: 503
  10. end
  11. end
  12. end

slices/health/view.rb

100.0% lines covered

100.0% branches covered

2 relevant lines. 2 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 module Health
  4. # The slice base view.
  5. 1 class View < Terminus::View
  6. end
  7. end

slices/health/views/context.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # auto_register: false
  2. # frozen_string_literal: true
  3. 1 module Health
  4. 1 module Views
  5. # The slice view context.
  6. 1 class Context < Hanami::View::Context
  7. 1 include Deps[main_assets: "main.assets"]
  8. end
  9. end
  10. end

slices/health/views/show.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Health
  3. 1 module Views
  4. # The show view.
  5. 1 class Show < Health::View
  6. 1 expose :color
  7. end
  8. end
  9. end