
```yml
{"filename": ".bundle/config"}
---
BUNDLE_PATH: "vendor/bundle"
```

```lruby
{"filename": "Gemfile"}
<<spdx_headers>>

source 'https://rubygems.org'

# Debug tools
gem 'awesome_print'

# Web-application prerequisites
gem 'puma'
gem 'sinatra'
gem 'rackup'

# SQL storage backend
gem 'sequel'
gem 'sqlite3'

<<rubocop_dependencies>>
```

```lruby
{"filename": "app.rb"}
#!/usr/bin/env ruby
<<spdx_headers>>

require 'securerandom'
require 'open3'
require 'sinatra'
require 'sequel'

<<start_and_monitor>>

DATABASE_FILENAME = 'store.db'

def make_sqlite_connection
  db = Sequel.connect("sqlite://#{DATABASE_FILENAME}")
<<create_table_if_exists>>
  db
end

configure do
  set :bind, '0.0.0.0'
  set :sequel_conn, make_sqlite_connection
end

<<requesting_a_new_task>>

<<dashboard_backend>>

<<full_logs_backend>>

<<short_logs_backend>>
```

```lruby
{"name": "create_table_if_exists"}
  db.create_table? :tasks do
    String :id, primary_key: true, null: false
    String :command, null: false
    String :cwd, null: false
    DateTime :created_at, default: Sequel::CURRENT_TIMESTAMP, null: false
    DateTime :finished_at, null: true
    String :return_message, null: true
  end
  db.create_table? :buffers do
    primary_key :id
    foreign_key :task_id, :tasks
    Integer :buffer_type
    Integer :sequence_number
    String :message, null: false
    String :created_at, default: Sequel::CURRENT_TIMESTAMP, null: false
  end
  db.create_table? :commands do
    primary_key :id
    String :command
    String :cwd
  end
```

## The dashboard

```lruby
{"name": "dashboard_backend"}
get '/' do
  commands = settings.sequel_conn[:commands]
                     .order(:id)

  tasks = settings.sequel_conn[:tasks]
                  .order(Sequel.desc(:created_at))
                  .limit(100)

  erb :index_tasks, locals: {
    tasks: tasks,
    commands: commands
  }
end
```

```erb
{"filename": "views/index_tasks.erb"}
<%#
<<spdx_headers>>
%>
<h1 class="text-3xl font-bold p-10">Supervised processes</h1>

<h2 class="text-xl font-bold">Available commands</h2>

<table class="border-2 border-black">
  <tr>
    <% commands.columns.each do |column_name| %>
      <th><%= column_name %></th>
    <% end%>
    <th>Launch</th>
  </tr>
  <% commands.each do |row| %>
    <tr class="odd=bg-white even:bg-gray-100 hover:bg-sky-700 hover:text-white">
      <% row.each do |cell| %> 
        <td class="p-2"><%= cell[1] %></td>
      <% end %>
      <td>
        <form action="/newtask/<%= row.first[1] %>" method="post">
          <button type="submit">Start!</button>
        </form>
      </td>
    </tr>
  <% end %>
</table>

<h2 class="text-xl font-bold">Launched commands</h2>

<table class="border-2 border-black">
  <tr>
    <% tasks.columns.each do |column_name| %>
      <th><%= column_name %></th>
    <% end%>
    <th>Full logs</th>
    <th>Short logs</th>
  </tr>
  <% tasks.each do |row| %>
    <tr class="odd=bg-white even:bg-gray-100 hover:bg-sky-700 hover:text-white">
      <% row.each do |cell| %> 
        <td class="p-2"><%= cell[1] %></td>
      <% end %>
      <td>
        <a href="/getFullLogs/<%= row.first[1].to_s %>">🔗</a>
      </td>
      <td>
        <a href="/getShortLogs/<%= row.first[1].to_s %>">🔗</a>
      </td>
    </tr>
  <% end %>
</table>
```

## Launching a task

Executing a task changes the status of the system, so it doesn't fit the pattern
of writing a `GET` handler.

Instead, we write a handler for a `POST`-type request, that reads the parameters of the
requested task (the command to run and its directory), generates an unique ID, and
inserts those informations in the database.

```lruby
{"name": "requesting_a_new_task"}
post '/newtask/:cmdid' do
  row = settings.sequel_conn[:commands]
                .where(id: params[:cmdid])
                .first

  cmd = row[:command]
  opts = { chdir: row[:cwd] }
  execid = SecureRandom.uuid

  settings.sequel_conn[:tasks]
          .insert(
            id: execid,
            command: cmd,
            cwd: opts[:chdir]
          )

  # Start a separate thread that executes the command, writes to the database
  # all the command's output, and waits for the command to finish.
  Thread.new do
    start_and_monitor_cmd(cmd.split, opts, execid, settings.sequel_conn)
  end

  # While the background thread is working, this request finishes with a
  # redirect-response towards the browser.
  redirect "/getFullLogs/#{execid}"
end
```

The core of this application is based on code from <https://dmytro.sh/blog/on-dangers-of-open3-popen3/>.

We use sequence-numbers because the `created_at` timestamps have 1-second resolution, and if the application produces multiple messages in a second, those messages might be displayed out-of-order. With sequence-numbers, we can reliably show the messages in the same order they were printed.

```lruby
{"name": "start_and_monitor"}
def start_and_monitor_cmd(cmd, opts, taskid, db_conn)
  #
  # Taken from https://dmytro.sh/blog/on-dangers-of-open3-popen3/
  #
  Open3.popen3(*cmd, opts) do |child_stdin, child_stdout, child_stderr, child_return|
    child_stdin.close
    readables = [child_stdout, child_stderr]
    sequence_number = 0
    until readables.empty?
      ready_pipes, = IO.select(readables) # Blocking call

      if ready_pipes.include? child_stdout
        stdout_buf = child_stdout.read_nonblock(4096, exception: false)
        if stdout_buf.nil?
          # Child's STDOUT has reached the end-of-file. Stop reading from it.
          readables.delete(child_stdout)
        else
          db_conn[:buffers].insert(
            task_id: taskid,
            buffer_type: 0,
            sequence_number: sequence_number,
            message: stdout_buf
          )

          sequence_number += 1
        end
      end
<<disable_rubocop_next>>
      if ready_pipes.include? child_stderr
        stderr_buf = child_stderr.read_nonblock(4096, exception: false)
        if stderr_buf.nil?
          # Child's STDERR has reached the end-of-file. Stop reading from it.
          readables.delete(child_stderr)
        else
          db_conn[:buffers].insert(
            task_id: taskid,
            buffer_type: 1,
            sequence_number: sequence_number,
            message: stderr_buf
          )

          sequence_number += 1
        end
      end
<<enable_rubocop_next>>
    end
    return_status = child_return.value # Blocking call

    db_conn[:tasks].where(id: taskid)
                   .update(
                     finished_at: Time.now,
                     return_message: return_status.to_s
                   )
  end
end
```

## Viewing the full logs of a task

On the backend, we create a handler for the `GET` requests to the path
`/getFullLogs/:execid` (where `:execid` is a parameter accessed under the name `params[:execid]`).
We query the database for the buffers associated with that task, and push that result-set to the `:view_task` frontend template.

```lruby
{"name": "full_logs_backend"}
get '/getFullLogs/:execid' do
  buffer_result = settings.sequel_conn[:buffers]
                          .where(task_id: params[:execid])
                          .order(:sequence_number)
                          .all

  erb :view_task, locals: {
    buffer: buffer_result
  }
end
```

On the frontend side, we plug the data from the result set into an HTML-formatted table,
one row at a time.

```erb
{"filename": "views/view_task.erb"}
<%#
<<spdx_headers>>
%>
<table class="border-2 border-black">
  <tr>
    <th>Timestamp</th>
    <th>Content</th>
    <th>Type</th>
  </tr>
  <% buffer.each do |row| %>
    <tr class="odd=bg-white even:bg-gray-100 hover:bg-sky-700 hover:text-white">
      <td class="p-2">
        <%= row[:created_at] %>
      </td>
      <td class="p-2">
        <%= Rack::Utils.escape_html(row[:message]).gsub("\n", '<br/>') %>
      </td>
      <td class="p-2">
        <%= (row[:buffer_type] == 0) ? "STDOUT" : "STDERR" %>
      </td>
    </tr>
  <% end %>
</table>
```

## Viewing most recent logs of a task

Some long-running tasks acummulate a lot of output-lines.
We write a handler for `/getShortLogs/:execid`, that shows
just the last 100 rows.

The frontend template used for rendering is reused.

```lruby
{"name": "short_logs_backend"}
get '/getShortLogs/:execid' do
  buffer_result = settings.sequel_conn[:buffers]
                          .where(task_id: params[:execid])
                          .order(Sequel.desc(:sequence_number))
                          .limit(7)
                          .all
                          .reverse

  erb :view_task, locals: {
    buffer: buffer_result
  }
end
```

## Annex - Rubocop linting

[RuboCop](https://rubocop.org/) is a Ruby linter: it flags strange or awkward snippets of source-code, and sometimes it even can correct them.

To add RuboCop to a project, we first add it as dependency on a Gemfile, with a separate gem-group (called `development`). This makes RuboCop not a "strong" dependency but an optional one (ignored by default).

```lruby
{"name": "rubocop_dependencies"}
# Rubocop linting
group :development, optional: true do
  gem 'rubocop'
  gem 'rubocop-performance'
  gem 'rubocop-sequel'
end
```

To install it, we must first "unlock" the `development` group of dependencis, install it, and then run it.

```bash
# Enable and install the "development" group of dependencies
bundle config set with 'development'
bundle install
# Look for formatting issues
bundle exec rubocop
# Auto-fix formatting issues
bundle exec rubocop -A
```

Although the enforced [coding style](https://rubystyle.guide/) is quite good, some rules are not fitting the project, and we can disable them. Every project can configure which rules are applied, by configuring the tool.

```yaml
{"filename": ".rubocop.yml"}
AllCops:
  NewCops: enable

plugins:
  - rubocop-sequel
  - rubocop-performance

# Having complex code is not a problem. Sometimes we solve complex problems.
Metrics/AbcSize:
  Enabled: false
Metrics/BlockLength:
  Enabled: false
Metrics/MethodLength:
  Enabled: false

# Ignore frozen-string warnings.
Style/FrozenStringLiteralComment:
  Enabled: false
Style/MutableConstant:
  Enabled: false

# This project's Gemfile lists dependencies in the logical order.
# Ordering them alphabetically would make no sense.
Bundler/OrderedGems:
  Enabled: false

# Tech limitation.
Lint/ScriptPermission:
  Enabled: false
```

Often we want to disable a rubocop rule just in a snippet of code. For this we can add a comment containing the rule's name. Below we disable the `Style/Next` rule.

```lruby
{"name": "disable_rubocop_next"}
      # rubocop:disable Style/Next
```

```lruby
{"name": "enable_rubocop_next"}
      # rubocop:enable Style/Next
```

```erb
{"filename": "views/layout.erb"}
<%#
<<spdx_headers>>
%>
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="/css/output.css" rel="stylesheet">
  </head>
  <body>
    <main class="container mx-auto mt-28 mb-32">
      <%= yield %>
    </main>
  </body>
</html>
```

```lruby
{"filename": "sequeldump.rb"}
#!/usr/bin/env ruby
<<spdx_headers>>

require 'awesome_print'
require 'sequel'

DATABASE_FILENAME = 'store.db'
TASK = "17efecd7-f636-4c66-8093-b34833cde437"
#tasks = Sequel.connect("sqlite://#{DATABASE_FILENAME}")[:tasks]
#tasks.each do |row|
#  ap row
#end

buffer = Sequel.connect("sqlite://#{DATABASE_FILENAME}")[:buffers]
  .where(task_id: TASK)
  .where(
    Sequel.|(
      Sequel.ilike(:message, '%reply from server%'),
      Sequel.ilike(:message, '%error%'),
      Sequel.ilike(:message, '%warn%')
    )
  )
  .order(:created_at)

buffer.each do |row|
  ap row
end
```

## Annex - The license

This project attepts to be mostly-compliant with the [REUSE Software](https://reuse.software/) specifications.

One requirement of the specification is to have the text of the chosen license (**MIT** in our case) in a file in the `LICENSES` directory. As such, we add it in:

```
{"filename": "LICENSES/MIT.txt"}
MIT License

Copyright (c) <year> <copyright holders>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.
```

Another prerequisite of the specification is that every copyrightable file must include
two comments using a special formatting, containing the license and the author/copyright holder.

```lruby
{"name": "spdx_headers"}
# SPDX-FileCopyrightText: 2026 PersonalCompute.Net <publisher@PersonalCompute.Net>
# SPDX-License-Identifier: MIT
```

We can check the REUSE compliance using the lint tool:

```bash
pipx run reuse lint
```

## Updating the CSS

Since the stylesheet doesn't change that often, the minified CSS
is stored in the repo.

To regenerate it, download the standalong tool form
<https://github.com/tailwindlabs/tailwindcss/releases>
and run it in the main directory.

```bash
chmod +x ~/Downloads/tailwindcss-linux-x64                                            
~/Downloads/tailwindcss-linux-x64 -i ./public/css/input.css -o ./public/css/output.css --minify
```

```css
{"filename": "public/css/input.css"}
@import "tailwindcss";
```

```css
{"filename": "public/css/output.css"}
/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-font-weight:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-sky-700:oklch(50% .134 242.749);--color-gray-100:oklch(96.7% .003 264.542);--color-black:#000;--color-white:#fff;--spacing:.25rem;--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-bold:700;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.static\!{position:static!important}.start-1{inset-inline-start:calc(var(--spacing) * 1)}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.mt-28{margin-top:calc(var(--spacing) * 28)}.mb-32{margin-bottom:calc(var(--spacing) * 32)}.\!block{display:block!important}.block{display:block}.contents{display:contents}.hidden{display:none}.inline{display:inline}.table{display:table}.shrink{flex-shrink:1}.grow{flex-grow:1}.border-collapse{border-collapse:collapse}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.resize{resize:both}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.rounded{border-radius:.25rem}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-black{border-color:var(--color-black)}.p-2{padding:calc(var(--spacing) * 2)}.p-10{padding:calc(var(--spacing) * 10)}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.capitalize{text-transform:capitalize}.lowercase{text-transform:lowercase}.uppercase{text-transform:uppercase}.italic{font-style:italic}.ordinal{--tw-ordinal:ordinal;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.underline{text-decoration-line:underline}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.invert{--tw-invert:invert(100%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter\!{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)!important}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.\[rdoc-ref\:BigDecimal\@Not\+a\+Number\]{rdoc-ref:BigDecimal@Not+a+Number}.\[ruby-dev\:28445\]{ruby-dev:28445}.even\:bg-gray-100:nth-child(2n){background-color:var(--color-gray-100)}@media (hover:hover){.hover\:bg-sky-700:hover{background-color:var(--color-sky-700)}.hover\:text-white:hover{color:var(--color-white)}}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}
```
