skadi 0.4.0.beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +41 -0
- data/LICENSE.md +22 -0
- data/README.md +111 -0
- data/app/assets/builds/dashboard.css +2 -0
- data/app/assets/builds/dashboard.js +36 -0
- data/app/assets/builds/skadi.js +1 -0
- data/app/controllers/skadi/application_controller.rb +4 -0
- data/app/controllers/skadi/asset_controller.rb +28 -0
- data/app/controllers/skadi/dashboard_controller.rb +105 -0
- data/app/controllers/skadi/tracking_controller.rb +106 -0
- data/app/helpers/skadi/application_helper.rb +38 -0
- data/app/models/skadi/application_record.rb +5 -0
- data/app/models/skadi/dashboard.rb +83 -0
- data/app/models/skadi/dashboard_query.rb +314 -0
- data/app/models/skadi/dashboard_validator.rb +269 -0
- data/app/models/skadi/demographic.rb +31 -0
- data/app/models/skadi/event.rb +33 -0
- data/app/models/skadi/helpers/sql.rb +86 -0
- data/app/models/skadi/schema.rb +135 -0
- data/app/models/skadi/view.rb +9 -0
- data/app/models/skadi/visit.rb +59 -0
- data/app/views/skadi/dashboard/_layout.html.erb +21 -0
- data/app/views/skadi/dashboard/show.html.erb +18 -0
- data/config/routes.rb +14 -0
- data/lib/generators/skadi/install/USAGE +20 -0
- data/lib/generators/skadi/install/install_generator.rb +46 -0
- data/lib/generators/skadi/install/templates/skadi_migration.rb.erb +111 -0
- data/lib/skadi/analytics.rb +35 -0
- data/lib/skadi/anonymity_set.rb +38 -0
- data/lib/skadi/configuration.rb +232 -0
- data/lib/skadi/controller_delegate.rb +322 -0
- data/lib/skadi/cookie_manager.rb +81 -0
- data/lib/skadi/engine.rb +38 -0
- data/lib/skadi/url.rb +66 -0
- data/lib/skadi/user_agent.rb +458 -0
- data/lib/skadi/version.rb +3 -0
- data/lib/skadi.rb +26 -0
- metadata +203 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9ae7555f1567742503930fd0134edb3612c61d540786e7734864e4568f53262c
|
|
4
|
+
data.tar.gz: 43c79b8e8f3483601f69529cec6d062b1c25f63e19cbee3cb0d7097d491dca93
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 2bed35cd3dfaaec1cdb8e6baf9ddc8e365cdafa18ac3d3e4c98c6f18075d9d9b86347ca5b2bd8b4851f4c4e971535a406ccc4cb57716752ef43ca2061b249f34
|
|
7
|
+
data.tar.gz: 6706dcb49b7b12b6042b76d271f2fd897700a23cc7702db71ab43af3bb38980fdfa3c7a29464341abe3074220ce5758cceefee542951f4c22888d78eb7a60ae6
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
## 0.4.0.beta.1 [2026-08-19]
|
|
2
|
+
- Add the dashboard
|
|
3
|
+
- Breaking: single consent and opt-out changed to more granular consent
|
|
4
|
+
- Breaking: user_method configuration option changed to user_controller_method
|
|
5
|
+
- DB: skadi_views.referrer removed
|
|
6
|
+
- DB: skadi_visits.cookies_enabled added to track cookie consent for visits
|
|
7
|
+
- DB: added additional indexes
|
|
8
|
+
- DB: added skadi_dashboards table
|
|
9
|
+
- Tracking script: fix requests not being queued
|
|
10
|
+
- Tracking script: fix FCP not being recorded in Chrome
|
|
11
|
+
- Tracking script: LCP now sent on page_hide instead of after a delay
|
|
12
|
+
- Refactor asset controller to use ActionController::Metal
|
|
13
|
+
- Fix skadi_tag crashing without a view
|
|
14
|
+
- Development: migrated from standard to rubocop-rails-omakase plus customisations
|
|
15
|
+
- Development: added biome and svelte-check to the front end
|
|
16
|
+
|
|
17
|
+
## 0.3.0 [2026-06-24]
|
|
18
|
+
- Add user agent parser for browser, engine and operating system detection
|
|
19
|
+
- Add bot detection with a configurable `track_bots` option to exclude crawler traffic (bot tracking is disabled by default in production)
|
|
20
|
+
- Record browser, engine and operating system demographics automatically for new visits
|
|
21
|
+
- Refactor tracking into a `ControllerDelegate`, with cookie handling consolidated in a new `CookieManager`
|
|
22
|
+
- Generate a fresh tracking token on consent so visitors sharing an IP and user agent are no longer linked
|
|
23
|
+
- Ensure analytics failures can never interrupt the host application's requests
|
|
24
|
+
- Harden referrer and exit-page URL redaction against malformed input, with a configurable length limit
|
|
25
|
+
- Validate the cache store used for anonymity sets, warning or disabling when it is unsupported
|
|
26
|
+
- Renew cookies on every request
|
|
27
|
+
- Expand unit and integration test coverage
|
|
28
|
+
|
|
29
|
+
## 0.2.0 [2026-06-13]
|
|
30
|
+
- Various security fixes
|
|
31
|
+
- Change defaults to be more privacy focused
|
|
32
|
+
- Fix views not being associated with visits
|
|
33
|
+
- Change skadi_tag default to :inline
|
|
34
|
+
- Add rate limiting
|
|
35
|
+
- Add minimum supported versions for Ruby (3.3) and Rails (7.2)
|
|
36
|
+
- Add additional details to readme
|
|
37
|
+
|
|
38
|
+
## 0.1.0 [2026-06-07]
|
|
39
|
+
- Scaffold gem
|
|
40
|
+
- Add tracking controller
|
|
41
|
+
- Add tracking front-end script
|
data/LICENSE.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2025 Simon J.
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Skadi
|
|
2
|
+
|
|
3
|
+
First-party, privacy-by-default analytics for Rails.
|
|
4
|
+
|
|
5
|
+
Skadi adds flexible and lightweight first-party analytics to your Rails app. Track page views and events, perform A/B testing and more, giving you all the information you need to improve your website.
|
|
6
|
+
|
|
7
|
+
Skadi is privacy-by-default, which means it respects users' privacy without any additional configuration. It gives you the power to be compliant with privacy laws*, while still collecting useful usage data about your site.
|
|
8
|
+
|
|
9
|
+
_* See [compliance section](#compliance)_
|
|
10
|
+
|
|
11
|
+
> [!NOTE]
|
|
12
|
+
> This is a human-written project with limited AI assistance. See the [AI Disclaimer](#ai-disclaimer) for details.
|
|
13
|
+
|
|
14
|
+
## Dependencies
|
|
15
|
+
|
|
16
|
+
This gem requires Rails >= 7.2 and Ruby >= 3.3.
|
|
17
|
+
|
|
18
|
+
This gem has no dependencies on other packages when installed as a gem.
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
Install the gem:
|
|
23
|
+
|
|
24
|
+
```shell
|
|
25
|
+
bundle add skadi
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Generate and run the database migration:
|
|
29
|
+
|
|
30
|
+
```shell
|
|
31
|
+
rails generate skadi:install
|
|
32
|
+
rails db:migrate
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Add Skadi to your application controller:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
class ApplicationController
|
|
39
|
+
include Skadi::Analytics
|
|
40
|
+
end
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Add the Skadi tracking tag to your layout:
|
|
44
|
+
|
|
45
|
+
```erb
|
|
46
|
+
<%= skadi_tag %>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## AI Disclaimer
|
|
50
|
+
|
|
51
|
+
This gem is human-written, with AI used improve the quality of the code, corresponding to [level 3 to 4](https://www.visidata.org/blog/2026/ai/#self-assessed-ai-level-for-contributions) on the VisiData AI scale:
|
|
52
|
+
|
|
53
|
+
> **Level 3**: Human coded, bots assisted non-trivially
|
|
54
|
+
> **Level 4**: Human coded, bots helped significantly
|
|
55
|
+
|
|
56
|
+
AI was used for same-line autocomplete suggestions, research and code review. Humans made all the architectural decisions, logic and the resulting code.
|
|
57
|
+
|
|
58
|
+
## Why use Skadi?
|
|
59
|
+
|
|
60
|
+
> **Todo:**
|
|
61
|
+
> add why skadi is great
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
## Compliance
|
|
65
|
+
|
|
66
|
+
Skadi respects users' privacy without any additional configuration. This means that Skadi will not store any personal data or track individual users without explicit consent.
|
|
67
|
+
|
|
68
|
+
> [!NOTE]
|
|
69
|
+
> It is your responsibility to comply with any applicable privacy laws. Skadi does not provide any legal advice as to the compliance of your use of Skadi.
|
|
70
|
+
|
|
71
|
+
### Cookies
|
|
72
|
+
|
|
73
|
+
The __skadi_id__ cookie is used to store a unique identifier for a user. This identifier is used to track the user across multiple visits to the site. This cookie is set when the `window.skadi.consent()` function is called in the front end.
|
|
74
|
+
|
|
75
|
+
The __skadi_tracking_opt_out__ cookie is used to indicate the user has opted out of tracking. This is a necessary cookie and does not contain any user information. This cookie is set when the `window.skadi.optOut()` function is called in the front end.
|
|
76
|
+
|
|
77
|
+
### Anonymity Sets
|
|
78
|
+
|
|
79
|
+
Some DPAs provide exceptions that allow for limited tracking of users before they consent, provided that the data is aggregated within a short time-frame. For example, the [Statistical Purposes exception](https://ico.org.uk/for-organisations/direct-marketing-and-privacy-and-electronic-communications/guidance-on-the-use-of-storage-and-access-technologies/what-are-the-exceptions/#statistical) for the UK GDPR.
|
|
80
|
+
|
|
81
|
+
Skadi allows you to enable the use of pseudonymous "anonymity sets" that can track a user's visit across multiple pages throughout a day:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
Skadi.configure do |config|
|
|
85
|
+
config.use_anonymity_sets = true
|
|
86
|
+
end
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
This anonymity set is calculated using a hash of the User's IP Address, their User Agent, and a random value called a pepper.
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
anonymity_set = hash(ip_address + user_agent + pepper)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Although the resulting value contains no personal information, it can still be used to track users, so it is considered pseudonymous rather than truly anonymous, and thus still subject to privacy laws. Once the pepper has been discarded, the anonymity set can no longer track users; the pepper is rotated and discarded daily, making the data collected fully anonymous.
|
|
96
|
+
|
|
97
|
+
Many privacy laws also require that users be given the option to opt-out of anonymisation-set tracking. Calling the `window.skadi.optOut()` function in the front end will set a cookie that will prevent Skadi from generating anonymisation sets for that user. Their views and events will still be collected, but no personal data will be stored.
|
|
98
|
+
|
|
99
|
+
## Configuration
|
|
100
|
+
|
|
101
|
+
> **Todo:**
|
|
102
|
+
> add configuration details
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
## Todo
|
|
108
|
+
|
|
109
|
+
* Document how to redact the current view path if it contains PII, e.g. paths like `/password-reset/<private token>`
|
|
110
|
+
* Document sequential events can still be re-derived based on `id`
|
|
111
|
+
* Document cache storage/TTL requirements or supply a job to clear the cache
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
|
|
2
|
+
@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-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight: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-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;--tw-ease:initial;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, 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-red-50:oklch(97.1% .013 17.38);--color-red-500:oklch(63.7% .237 25.331);--color-red-800:oklch(44.4% .177 26.899);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-black:#000;--color-white:#fff;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--font-weight-light:300;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--leading-normal:1.5;--ease-in-out:cubic-bezier(.4, 0, .2, 1);--aspect-video:16 / 9;--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);--color-ice-50:oklch(97.4% .02 247.89);--color-ice-100:oklch(95% .022 238.64);--color-ice-200:oklch(87.6% .046 233.06);--color-ice-300:oklch(81.6% .078 224.93);--color-ice-400:oklch(72.8% .078 228.47);--color-ice-600:oklch(54% .064 238.76);--color-ice-700:oklch(45.4% .057 244.01);--color-ice-800:oklch(36.2% .049 247.19);--color-ice-950:oklch(22.5% .038 257.16);--color-dawn-50:oklch(95.8% .023 48.5);--color-dawn-100:oklch(92.6% .041 46.7);--color-dawn-200:oklch(85.2% .086 44);--color-dawn-300:oklch(78% .136 43.7);--color-dawn-600:oklch(54.9% .194 34.66);--color-dawn-700:oklch(47.2% .193 29.23);--color-dawn-900:oklch(31.1% .098 40.03);--color-night-50:oklch(96.6% .016 278.57);--color-night-100:oklch(92.5% .037 280.64);--color-night-400:oklch(71.7% .154 285.62);--color-night-700:oklch(52.5% .293 292.3);--color-night-800:oklch(40.1% .23 287.69);--color-night-900:oklch(27.1% .162 282.12);--color-night-950:oklch(21.4% .135 274.82);--spacing-prose:min(100%, 48rem)}}@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,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, 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;-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:where(:not(iframe)){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}body{--tw-gradient-position:to top}@supports (background-image:linear-gradient(in lab, red, red)){body{--tw-gradient-position:to top in oklab}}body{background-image:linear-gradient(var(--tw-gradient-stops));--tw-gradient-from:#ffede40d}@supports (color:color-mix(in lab, red, red)){body{--tw-gradient-from:color-mix(in oklab, var(--color-dawn-50) 5%, transparent)}}body{--tw-gradient-via:#9c93ff0d}@supports (color:color-mix(in lab, red, red)){body{--tw-gradient-via:color-mix(in oklab, var(--color-night-400) 5%, transparent)}}body{--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops);--tw-gradient-to:#4a74900d}@supports (color:color-mix(in lab, red, red)){body{--tw-gradient-to:color-mix(in oklab, var(--color-ice-600) 5%, transparent)}}body{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));padding-bottom:calc(var(--spacing) * 32)}p{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}label{gap:calc(var(--spacing) * .5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);flex-direction:column;display:flex}.help-text{max-width:calc(var(--spacing) * 128);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal);color:var(--color-gray-500)}input,select{max-width:calc(var(--spacing) * 120);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-night-100);padding-inline:calc(var(--spacing) * 2.5);padding-block:calc(var(--spacing) * 2);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-leading:var(--leading-normal);line-height:var(--leading-normal);flex-grow:1}[contenteditable]{max-width:calc(var(--spacing) * 120)}button:where(:not(.unstyled)){--tw-translate-x:0px;--tw-translate-y:0px;width:max-content;translate:var(--tw-translate-x) var(--tw-translate-y);cursor:pointer;align-items:center;gap:calc(var(--spacing) * 2);border-bottom-style:var(--tw-border-style);border-color:#2840561a;border-bottom-width:4px;display:flex}@supports (color:color-mix(in lab, red, red)){button:where(:not(.unstyled)){border-color:color-mix(in oklab, var(--color-ice-800) 10%, transparent)}}button:where(:not(.unstyled)){background-color:var(--color-ice-100);padding-inline:calc(var(--spacing) * 4);padding-top:calc(var(--spacing) * 1.5);padding-bottom:var(--spacing);font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-ice-950)}button:where(:not(.unstyled)):hover,button:where(:not(.unstyled)):active{background-color:var(--color-ice-50);color:var(--color-black)}button:where(:not(.unstyled)):active{--tw-translate-y:var(--spacing);translate:var(--tw-translate-x) var(--tw-translate-y);clip-path:inset(0 0 .25rem)}button:where(:not(.unstyled)).sm{gap:var(--spacing);padding-inline:calc(var(--spacing) * 3);padding-top:var(--spacing);padding-bottom:calc(var(--spacing) * .75);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}button:where(:not(.unstyled)).emph{border-color:#00000040}@supports (color:color-mix(in lab, red, red)){button:where(:not(.unstyled)).emph{border-color:color-mix(in oklab, var(--color-black) 25%, transparent)}}button:where(:not(.unstyled)).emph{background-color:var(--color-night-700);color:var(--color-white)}button:where(:not(.unstyled)).ghost{border-style:var(--tw-border-style);padding-block:calc(var(--spacing) * 2);color:var(--color-night-800);background-color:#0000;border-width:0}@media (hover:hover){button:where(:not(.unstyled)).ghost:hover{color:var(--color-night-900);--tw-backdrop-brightness:brightness(95%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}}button:where(:not(.unstyled)).ghost:active{translate:none}button:where(:not(.unstyled)).ghost{clip-path:none}button:where(:not(.unstyled)).ghost.sm{padding-block:var(--spacing)}[contenteditable=true]{width:var(--spacing-prose);cursor:text;border-left-style:var(--tw-border-style);--tw-border-style:dashed;border-style:dashed;border-left-width:1px;border-color:var(--color-gray-500);padding-left:calc(var(--spacing) * 3)}[contenteditable][placeholder]:empty:after{color:var(--color-gray-500);content:attr(placeholder);display:inline}#skadi-dashboard{width:calc(var(--spacing) * 256);max-width:100%;padding-inline:calc(var(--spacing) * 4);margin-inline:auto}nav{align-items:flex-start;gap:calc(var(--spacing) * 2);border-bottom-style:var(--tw-border-style);border-bottom-width:3px;border-color:var(--color-night-700);width:100%;display:flex}nav>button{margin-bottom:calc(var(--spacing) * -.5);white-space:nowrap;border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 3);padding-bottom:calc(var(--spacing) * 1.5);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);text-overflow:ellipsis;background-color:#0000;border-width:0;flex-shrink:1;display:block;overflow:hidden}nav>button:active{--tw-translate-x:0px;--tw-translate-y:0px;translate:var(--tw-translate-x) var(--tw-translate-y);clip-path:none}nav>button.selected{background-color:var(--color-night-700);padding-inline:calc(var(--spacing) * 4);--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-white);flex-shrink:0}nav>button:hover{background-color:var(--color-night-700);color:var(--color-white);flex-shrink:0}nav>button:active{background-color:var(--color-night-700);color:var(--color-night-50)}table{min-width:100%}table td,table th{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-night-100);padding-block:var(--spacing);padding-right:calc(var(--spacing) * 4);padding-left:calc(var(--spacing) * 2);text-align:left}table th{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}table tr:hover td{background-color:#f1f3ff66}@supports (color:color-mix(in lab, red, red)){table tr:hover td{background-color:color-mix(in oklab, var(--color-night-50) 40%, transparent)}}}@layer components;@layer utilities{.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.top-0{top:0}.top-1\/2{top:50%}.top-2{top:calc(var(--spacing) * 2)}.-right-4\.75{right:calc(var(--spacing) * -4.75)}.right-2{right:calc(var(--spacing) * 2)}.right-6{right:calc(var(--spacing) * 6)}.bottom-2{bottom:calc(var(--spacing) * 2)}.-left-5{left:calc(var(--spacing) * -5)}.left-0\.75{left:calc(var(--spacing) * .75)}.left-1\/2{left:50%}.left-auto{left:auto}.z-10{z-index:10}.col-span-2{grid-column:span 2/span 2}.col-span-4{grid-column:span 4/span 4}.mx-auto{margin-inline:auto}.my-1{margin-block:var(--spacing)}.-mt-1{margin-top:calc(var(--spacing) * -1)}.mt-1{margin-top:var(--spacing)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mt-12{margin-top:calc(var(--spacing) * 12)}.mt-px{margin-top:1px}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.-ml-1{margin-left:calc(var(--spacing) * -1)}.ml-1{margin-left:var(--spacing)}.ml-auto{margin-left:auto}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.table{display:table}.aspect-video{aspect-ratio:var(--aspect-video)}.size-4\.5{width:calc(var(--spacing) * 4.5);height:calc(var(--spacing) * 4.5)}.h-6{height:calc(var(--spacing) * 6)}.h-full{height:100%}.min-h-screen{min-height:100vh}.w-13{width:calc(var(--spacing) * 13)}.w-full{width:100%}.w-max{width:max-content}.max-w-160{max-width:calc(var(--spacing) * 160)}.max-w-256{max-width:calc(var(--spacing) * 256)}.max-w-full{max-width:100%}.min-w-0{min-width:0}.flex-1{flex:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.-translate-1\/2{--tw-translate-x:calc(calc(1 / 2 * 100%) * -1);--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-x-6\.5{--tw-translate-x:calc(var(--spacing) * -6.5);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-3\.25{--tw-translate-x:calc(var(--spacing) * 3.25);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-6\.5{--tw-translate-x:calc(var(--spacing) * 6.5);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.cursor-pointer{cursor:pointer}.grid-flow-col{grid-auto-flow:column}.grid-cols-\[auto_4rem_1fr_auto\]{grid-template-columns:auto 4rem 1fr auto}.grid-rows-\[0fr\]{grid-template-rows:0fr}.grid-rows-\[1fr\]{grid-template-rows:1fr}.flex-col{flex-direction:column}.flex-row{flex-direction:row}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:var(--spacing)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.rounded-full{border-radius:2147483647px}.border{border-style:var(--tw-border-style);border-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-l-4{border-left-style:var(--tw-border-style);border-left-width:4px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-black\/20{border-color:#0003}@supports (color:color-mix(in lab, red, red)){.border-black\/20{border-color:color-mix(in oklab, var(--color-black) 20%, transparent)}}.border-dawn-200{border-color:var(--color-dawn-200)}.border-dawn-600{border-color:var(--color-dawn-600)}.border-gray-600{border-color:var(--color-gray-600)}.border-ice-100{border-color:var(--color-ice-100)}.border-ice-200{border-color:var(--color-ice-200)}.border-ice-400{border-color:var(--color-ice-400)}.border-ice-600{border-color:var(--color-ice-600)}.border-ice-700{border-color:var(--color-ice-700)}.border-night-50{border-color:var(--color-night-50)}.border-night-100{border-color:var(--color-night-100)}.border-night-700{border-color:var(--color-night-700)}.border-red-500{border-color:var(--color-red-500)}.border-red-800{border-color:var(--color-red-800)}.bg-dawn-50{background-color:var(--color-dawn-50)}.bg-dawn-100{background-color:var(--color-dawn-100)}.bg-dawn-300{background-color:var(--color-dawn-300)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-600{background-color:var(--color-gray-600)}.bg-ice-50{background-color:var(--color-ice-50)}.bg-ice-300{background-color:var(--color-ice-300)}.bg-ice-600{background-color:var(--color-ice-600)}.bg-night-700{background-color:var(--color-night-700)}.bg-red-50\/50{background-color:#fef2f280}@supports (color:color-mix(in lab, red, red)){.bg-red-50\/50{background-color:color-mix(in oklab, var(--color-red-50) 50%, transparent)}}.bg-white{background-color:var(--color-white)}.fill-current{fill:currentColor}.p-1{padding:var(--spacing)}.p-2{padding:calc(var(--spacing) * 2)}.p-4{padding:calc(var(--spacing) * 4)}.px-0\.5{padding-inline:calc(var(--spacing) * .5)}.px-1{padding-inline:var(--spacing)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-8{padding-block:calc(var(--spacing) * 8)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pr-4{padding-right:calc(var(--spacing) * 4)}.pr-8{padding-right:calc(var(--spacing) * 8)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pl-4{padding-left:calc(var(--spacing) * 4)}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.625rem\]{font-size:.625rem}.leading-none{--tw-leading:1;line-height:1}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-light{--tw-font-weight:var(--font-weight-light);font-weight:var(--font-weight-light)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.whitespace-pre-wrap{white-space:pre-wrap}.text-dawn-700{color:var(--color-dawn-700)}.text-dawn-900\/65{color:#571900a6}@supports (color:color-mix(in lab, red, red)){.text-dawn-900\/65{color:color-mix(in oklab, var(--color-dawn-900) 65%, transparent)}}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-ice-600{color:var(--color-ice-600)}.text-ice-700{color:var(--color-ice-700)}.text-ice-800{color:var(--color-ice-800)}.text-night-700{color:var(--color-night-700)}.text-night-800{color:var(--color-night-800)}.text-night-950{color:var(--color-night-950)}.text-red-800{color:var(--color-red-800)}.text-white{color:var(--color-white)}.capitalize{text-transform:capitalize}.opacity-25{opacity:.25}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-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,)}.transition-\[grid-template-rows\]{transition-property:grid-template-rows;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}@media (hover:hover){.group-hover\:text-black:is(:where(.group):hover *){color:var(--color-black)}.group-hover\:text-night-800:is(:where(.group):hover *){color:var(--color-night-800)}.hover\:border-night-700:hover{border-color:var(--color-night-700)}.hover\:bg-night-50:hover{background-color:var(--color-night-50)}.hover\:bg-transparent:hover{background-color:#0000}.hover\:text-black:hover{color:var(--color-black)}.hover\:backdrop-filter-none:hover{-webkit-backdrop-filter:none;backdrop-filter:none}}@media (width>=40rem){.sm\:sticky{position:sticky}.sm\:-mb-4{margin-bottom:calc(var(--spacing) * -4)}.sm\:contents{display:contents}.sm\:h-0{height:0}.sm\:justify-start{justify-content:flex-start}}@media (width>=48rem){.md\:flex-row{flex-direction:row}.md\:items-start{align-items:flex-start}}@media (width>=64rem){.lg\:px-12{padding-inline:calc(var(--spacing) * 12)}}@media (width>=80rem){.xl\:full-width{width:calc(100vw - calc(2 * var(--full-width-margin,2rem)));max-width:none;margin-left:calc(-50vw + (var(--full-width-margin,2rem)));margin-right:calc(-50vw + (var(--full-width-margin,2rem)));position:relative;left:50%;right:50%}.xl\:max-w-384{max-width:calc(var(--spacing) * 384)}.xl\:flex-row{flex-direction:row}.xl\:items-start{align-items:flex-start}.xl\:justify-center{justify-content:center}}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{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-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}@property --tw-ease{syntax:"*";inherits:false}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"<length-percentage>";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"<length-percentage>";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"<length-percentage>";inherits:false;initial-value:100%}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}
|