upright 0.3.0 → 0.4.0
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 +4 -4
- data/README.md +66 -7
- data/Rakefile +22 -0
- data/app/assets/images/upright/.keep +0 -0
- data/app/assets/stylesheets/upright/_global.css +2 -0
- data/app/assets/stylesheets/upright/artifact.css +7 -25
- data/app/assets/stylesheets/upright/flash.css +19 -0
- data/app/assets/stylesheets/upright/header.css +14 -0
- data/app/assets/stylesheets/upright/incidents.css +610 -0
- data/app/assets/stylesheets/upright/map.css +6 -0
- data/app/assets/stylesheets/upright/public_status.css +534 -0
- data/app/controllers/concerns/.keep +0 -0
- data/app/controllers/concerns/upright/authentication.rb +1 -0
- data/app/controllers/concerns/upright/proxy_authentication.rb +134 -0
- data/app/controllers/upright/alertmanager_proxy_controller.rb +12 -4
- data/app/controllers/upright/artifacts_controller.rb +1 -1
- data/app/controllers/upright/incidents/updates_controller.rb +13 -0
- data/app/controllers/upright/incidents_controller.rb +53 -0
- data/app/controllers/upright/prometheus_proxy_controller.rb +13 -18
- data/app/controllers/upright/public/base_controller.rb +16 -0
- data/app/controllers/upright/public/incidents_controller.rb +6 -0
- data/app/controllers/upright/public/service_incidents_controller.rb +7 -0
- data/app/controllers/upright/public/services_controller.rb +6 -0
- data/app/controllers/upright/sessions_controller.rb +36 -1
- data/app/controllers/upright/traces_controller.rb +88 -0
- data/app/helpers/upright/application_helper.rb +22 -2
- data/app/helpers/upright/artifacts_helper.rb +20 -0
- data/app/helpers/upright/probe_results_helper.rb +1 -0
- data/app/helpers/upright/public/services_helper.rb +70 -0
- data/app/javascript/upright/application.js +3 -0
- data/app/javascript/upright/controllers/sites_map_controller.js +13 -5
- data/app/javascript/upright/controllers/template_controller.js +11 -0
- data/app/javascript/upright/public.js +5 -0
- data/app/jobs/upright/health_metrics_job.rb +27 -0
- data/app/jobs/upright/incident_reporter_job.rb +7 -0
- data/app/jobs/upright/maintenance_advance_job.rb +8 -0
- data/app/jobs/upright/playwright_video_sweep_job.rb +21 -0
- data/app/jobs/upright/rollups/daily_aggregation_job.rb +14 -0
- data/app/models/concerns/.keep +0 -0
- data/app/models/concerns/upright/incidents/auto_reporting.rb +93 -0
- data/app/models/concerns/upright/incidents/lifecycle.rb +47 -0
- data/app/models/concerns/upright/playwright/form_authentication.rb +3 -2
- data/app/models/concerns/upright/playwright/lifecycle.rb +8 -13
- data/app/models/concerns/upright/playwright/trace_recording.rb +53 -0
- data/app/models/concerns/upright/playwright/video_recording.rb +55 -14
- data/app/models/concerns/upright/probe_result/stale_cleanup.rb +26 -2
- data/app/models/concerns/upright/probeable.rb +9 -1
- data/app/models/concerns/upright/services/live_status.rb +69 -0
- data/app/models/concerns/upright/services/maintenance_status.rb +19 -0
- data/app/models/upright/artifact.rb +2 -1
- data/app/models/upright/http/request.rb +12 -3
- data/app/models/upright/incident/automatic_report.rb +3 -0
- data/app/models/upright/incident.rb +87 -0
- data/app/models/upright/incident_affected_service.rb +9 -0
- data/app/models/upright/incident_update.rb +7 -0
- data/app/models/upright/maintenance.rb +36 -0
- data/app/models/upright/persistent_record.rb +12 -0
- data/app/models/upright/playwright/authenticator/base.rb +50 -70
- data/app/models/upright/probes/http_probe.rb +30 -1
- data/app/models/upright/probes/playwright/base.rb +2 -0
- data/app/models/upright/probes/status/probe.rb +8 -0
- data/app/models/upright/probes/status.rb +3 -11
- data/app/models/upright/probes/uptime.rb +2 -9
- data/app/models/upright/rollups/daily_uptime.rb +28 -0
- data/app/models/upright/rollups/probe_rollup.rb +64 -0
- data/app/models/upright/rollups/probe_uptime.rb +9 -0
- data/app/models/upright/rollups/site_uptime.rb +75 -0
- data/app/models/upright/service/daily_status.rb +39 -0
- data/app/models/upright/service/incident_history.rb +24 -0
- data/app/models/upright/service/outage.rb +15 -0
- data/app/models/upright/service/status_page.rb +35 -0
- data/app/models/upright/service.rb +101 -0
- data/app/models/upright/status.rb +21 -0
- data/app/models/upright/traceroute/ip_metadata_lookup.rb +21 -2
- data/app/models/upright/traceroute/result.rb +8 -1
- data/app/views/layouts/upright/_header.html.erb +12 -2
- data/app/views/layouts/upright/application.html.erb +4 -1
- data/app/views/layouts/upright/public.html.erb +18 -0
- data/app/views/layouts/upright/public.rss.builder +1 -0
- data/app/views/upright/active_storage/attachments/_attachment.html.erb +1 -1
- data/app/views/upright/artifacts/show.html.erb +8 -0
- data/app/views/upright/incidents/_form.html.erb +93 -0
- data/app/views/upright/incidents/_row.html.erb +15 -0
- data/app/views/upright/incidents/edit.html.erb +64 -0
- data/app/views/upright/incidents/index.html.erb +53 -0
- data/app/views/upright/incidents/new.html.erb +6 -0
- data/app/views/upright/public/_branding.html.erb +3 -0
- data/app/views/upright/public/_footer.html.erb +3 -0
- data/app/views/upright/public/incidents/show.html.erb +29 -0
- data/app/views/upright/public/service_incidents/_incident.html.erb +14 -0
- data/app/views/upright/public/service_incidents/index.html.erb +50 -0
- data/app/views/upright/public/services/_active_events.html.erb +28 -0
- data/app/views/upright/public/services/_degraded_list.html.erb +14 -0
- data/app/views/upright/public/services/_overall_banner.html.erb +4 -0
- data/app/views/upright/public/services/_service.html.erb +34 -0
- data/app/views/upright/public/services/_upcoming_maintenance.html.erb +21 -0
- data/app/views/upright/public/services/_uptime_bar.html.erb +9 -0
- data/app/views/upright/public/services/index.html.erb +13 -0
- data/app/views/upright/public/services/index.rss.builder +29 -0
- data/app/views/upright/sessions/new.html.erb +14 -1
- data/config/importmap.rb +25 -14
- data/config/initializers/date_formats.rb +5 -0
- data/config/initializers/mime_types.rb +1 -0
- data/config/routes.rb +26 -2
- data/db/migrate/20260629000001_fix_probe_result_duration_precision.rb +14 -0
- data/db/persistent_migrate/20260512000001_create_upright_rollups.rb +16 -0
- data/db/persistent_migrate/20260702000001_create_upright_incidents.rb +34 -0
- data/db/persistent_migrate/20260707000001_add_probe_identity_to_upright_rollups.rb +11 -0
- data/db/persistent_migrate/20260709000001_add_created_by_to_incidents.rb +7 -0
- data/db/persistent_migrate/20260901000001_add_auto_reporting_to_upright_incidents.rb +22 -0
- data/lib/generators/upright/install/database_config.rb +46 -0
- data/lib/generators/upright/install/install_generator.rb +18 -10
- data/lib/generators/upright/install/templates/Dockerfile +80 -0
- data/lib/generators/upright/install/templates/content_security_policy.rb +42 -0
- data/lib/generators/upright/install/templates/deploy.yml +13 -13
- data/lib/generators/upright/install/templates/development_prometheus.yml +1 -1
- data/lib/generators/upright/install/templates/docker-compose.yml +12 -11
- data/lib/generators/upright/install/templates/omniauth.rb +7 -3
- data/lib/generators/upright/install/templates/otel_collector.yml +6 -0
- data/lib/generators/upright/install/templates/recurring.yml +20 -0
- data/lib/generators/upright/install/templates/sites.yml +4 -0
- data/lib/generators/upright/install/templates/upright.rb +21 -2
- data/lib/generators/upright/install/templates/upright.rules.yml +11 -4
- data/lib/upright/configuration.rb +113 -6
- data/lib/upright/engine.rb +25 -2
- data/lib/upright/metrics.rb +25 -0
- data/lib/upright/site.rb +31 -1
- data/lib/upright/version.rb +3 -1
- data/lib/upright.rb +29 -2
- metadata +107 -7
- data/config/credentials/development.key +0 -1
- data/config/credentials/test.key +0 -1
- data/lib/tasks/upright_tasks.rake +0 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c91a962bc77007bffeebba4df5da09c213a42c46a416de5fce7d19e9169258f7
|
|
4
|
+
data.tar.gz: 47b63615e16a15aede26f738a3ffc7256fada1aab09b775ff7ddd90282c91ea0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 25a77e92224a4869f1da746deba9f5a4440c4a071d2e3b0c8fef363c2abd36e270cc9a1147a11081284a85f0deb8e22cc73a09d8a3dcd183ea29dd7a0419e872
|
|
7
|
+
data.tar.gz: a5e047ccb8feb9e864652496e81cdbb17ea4e0ae0972b8e380df9fa1206b011c23d5fd0b1e8bae2f366313dcb48b7af25d3babeba1f96e1211967bf70e954415
|
data/README.md
CHANGED
|
@@ -59,7 +59,7 @@ rails new my-upright --database=sqlite3 --skip-test
|
|
|
59
59
|
cd my-upright
|
|
60
60
|
bundle add upright
|
|
61
61
|
bin/rails generate upright:install
|
|
62
|
-
bin/rails db:
|
|
62
|
+
bin/rails db:prepare
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
Start the server:
|
|
@@ -76,7 +76,10 @@ Visit http://app.my-upright.localhost:3000 to see your Upright instance.
|
|
|
76
76
|
|
|
77
77
|
The `upright:install` generator creates:
|
|
78
78
|
|
|
79
|
+
- `config/database.yml` - Development and test split into `primary` (probe results), `persistent` (rollups and incidents, migrated from the gem) and `queue` (Solid Queue) databases
|
|
80
|
+
- `config/recurring.yml` - Probe schedules and the engine's housekeeping, health, rollup, incident and maintenance jobs
|
|
79
81
|
- `config/initializers/upright.rb` - Engine configuration
|
|
82
|
+
- `config/initializers/content_security_policy.rb` - Ready-to-enable CSP covering the engine's CDN dependencies
|
|
80
83
|
- `config/sites.yml` - Site definitions for each VPS you host Upright on
|
|
81
84
|
- `config/prometheus/prometheus.yml` - Prometheus configuration
|
|
82
85
|
- `config/alertmanager/alertmanager.yml` - AlertManager configuration
|
|
@@ -122,6 +125,8 @@ shared:
|
|
|
122
125
|
country: NL
|
|
123
126
|
geohash: u17982
|
|
124
127
|
provider: digitalocean
|
|
128
|
+
stores_metrics: true
|
|
129
|
+
primary: true
|
|
125
130
|
|
|
126
131
|
- code: sfo
|
|
127
132
|
city: San Francisco
|
|
@@ -132,14 +137,16 @@ shared:
|
|
|
132
137
|
|
|
133
138
|
Each site node identifies itself via the `SITE_SUBDOMAIN` environment variable, configured in your Kamal deploy.yml.
|
|
134
139
|
|
|
140
|
+
Two optional flags give a site a role beyond running probes: `primary` serves the app and status hostnames and runs the jobs writing the shared database, and `stores_metrics` runs a local Prometheus and Alertmanager. See [Sites and their roles](https://github.com/basecamp/upright/blob/main/docs/sites.md) for what each one changes, the health metrics every site exports, and how daily rollups read across them.
|
|
141
|
+
|
|
135
142
|
### Authentication
|
|
136
143
|
|
|
137
144
|
#### Static Credentials
|
|
138
145
|
|
|
139
|
-
Upright uses static credentials by default with username `admin` and password `
|
|
146
|
+
Upright uses static credentials by default with username `admin` and the password taken from the `ADMIN_PASSWORD` environment variable.
|
|
140
147
|
|
|
141
|
-
> [!
|
|
142
|
-
>
|
|
148
|
+
> [!IMPORTANT]
|
|
149
|
+
> There is no default password: `ADMIN_PASSWORD` must be set, and sign-in fails closed without it. Set it as a Kamal secret in production (the generated `config/deploy.yml` already lists it) and export it locally for development.
|
|
143
150
|
|
|
144
151
|
|
|
145
152
|
#### OpenID Connect
|
|
@@ -197,8 +204,14 @@ class PingProbe < FrozenRecord::Base
|
|
|
197
204
|
|
|
198
205
|
stagger_by_site 3.seconds
|
|
199
206
|
|
|
207
|
+
# Hostnames, IPv4, and IPv6 addresses only. The first character may not be
|
|
208
|
+
# a dash, so a configured host can never be mistaken for a ping flag.
|
|
209
|
+
HOST_PATTERN = /\A[a-z0-9:][a-z0-9:._-]*\z/i
|
|
210
|
+
|
|
200
211
|
def check
|
|
201
|
-
|
|
212
|
+
raise ArgumentError, "invalid ping host: #{host.inspect}" unless HOST_PATTERN.match?(host.to_s)
|
|
213
|
+
|
|
214
|
+
@ping_output, status = Open3.capture2e("ping", "-c", "1", "-W", "5", "--", host)
|
|
202
215
|
status.success?
|
|
203
216
|
end
|
|
204
217
|
|
|
@@ -425,6 +438,7 @@ proxy:
|
|
|
425
438
|
env:
|
|
426
439
|
secret:
|
|
427
440
|
- RAILS_MASTER_KEY
|
|
441
|
+
- ADMIN_PASSWORD
|
|
428
442
|
tags:
|
|
429
443
|
amsterdam:
|
|
430
444
|
SITE_SUBDOMAIN: ams
|
|
@@ -536,20 +550,65 @@ bin/dev
|
|
|
536
550
|
|
|
537
551
|
Visit http://app.upright.localhost:3000 and sign in with:
|
|
538
552
|
- **Username**: `admin`
|
|
539
|
-
- **Password**:
|
|
553
|
+
- **Password**: the value of the `ADMIN_PASSWORD` env var, e.g. `ADMIN_PASSWORD=upright bin/dev` (a throwaway value like this is fine for local testing only — never for a deployed site)
|
|
540
554
|
|
|
541
555
|
### Testing Playwright Probes
|
|
542
556
|
|
|
543
557
|
Run probes with a visible browser window:
|
|
544
558
|
|
|
545
559
|
```bash
|
|
546
|
-
|
|
560
|
+
HEADLESS=false bin/rails console
|
|
547
561
|
```
|
|
548
562
|
|
|
549
563
|
```ruby
|
|
550
564
|
Probes::Playwright::MyServiceAuthProbe.check
|
|
551
565
|
```
|
|
552
566
|
|
|
567
|
+
### Upgrading Playwright
|
|
568
|
+
|
|
569
|
+
Playwright versions are pinned via `Upright::PLAYWRIGHT_VERSION` in `lib/upright/version.rb`. This drives the Ruby gem and npm package versions. To upgrade:
|
|
570
|
+
|
|
571
|
+
1. Update `PLAYWRIGHT_VERSION` in `lib/upright/version.rb`
|
|
572
|
+
2. Update the version in `package.json`
|
|
573
|
+
3. Run `bin/setup` (or manually: `npm install && npx playwright install chromium`)
|
|
574
|
+
4. Run `bin/rails test` to verify compatibility
|
|
575
|
+
5. Commit the updated `package.json` and `package-lock.json`
|
|
576
|
+
|
|
577
|
+
### Viewing Playwright Traces
|
|
578
|
+
|
|
579
|
+
Upright stores a Playwright trace as a probe result artifact and does not serve
|
|
580
|
+
the Playwright Trace Viewer. The viewer renders a trace's tags and attributes as
|
|
581
|
+
HTML in its own origin, so serving it from Upright's hostname would let a trace
|
|
582
|
+
run script against an admin session.
|
|
583
|
+
|
|
584
|
+
A trace artifact links to `config.trace_viewer_url`, which defaults to
|
|
585
|
+
upstream's hosted viewer at `https://trace.playwright.dev`. That viewer runs
|
|
586
|
+
entirely in the browser, and being on its own registrable domain is what keeps a
|
|
587
|
+
trace's contents off Upright's origin.
|
|
588
|
+
|
|
589
|
+
Following the link hands that origin a URL it can read for 24 hours. Set
|
|
590
|
+
`config.trace_viewer_url` to a viewer you host to keep traces to yourself, or
|
|
591
|
+
assign `nil` to keep them download-only:
|
|
592
|
+
|
|
593
|
+
```bash
|
|
594
|
+
npx playwright show-trace trace.zip
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
A URL under `config.hostname` raises `Upright::ConfigurationError`, since that
|
|
598
|
+
would put the trace back on the admin origin.
|
|
599
|
+
|
|
600
|
+
That trace URL is served by `Upright::TracesController`, not Active Storage. The
|
|
601
|
+
viewer fetches it from its own origin, so the request arrives without the admin
|
|
602
|
+
session and needs `Access-Control-Allow-Origin` to be readable at all. The
|
|
603
|
+
controller sends that header for the configured viewer's origin and nothing
|
|
604
|
+
else, and takes a signed id in place of the session: scoped to a purpose,
|
|
605
|
+
expiring after 24 hours, and resolvable only to a blob attached to an
|
|
606
|
+
`Upright::ProbeResult` under a `.zip` filename. With no viewer configured the
|
|
607
|
+
route 404s, so it exists only where it's used.
|
|
608
|
+
|
|
609
|
+
Range requests are answered, which is how the viewer reads a ZIP's central
|
|
610
|
+
directory without downloading the whole archive.
|
|
611
|
+
|
|
553
612
|
### Running Tests
|
|
554
613
|
|
|
555
614
|
```bash
|
data/Rakefile
CHANGED
|
@@ -4,3 +4,25 @@ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
|
|
4
4
|
load "rails/tasks/engine.rake"
|
|
5
5
|
|
|
6
6
|
require "bundler/gem_tasks"
|
|
7
|
+
|
|
8
|
+
Dir.glob("lib/tasks/**/*.rake").each { |r| load r }
|
|
9
|
+
|
|
10
|
+
desc "Tag v<current version> and push it, which starts the Release workflow"
|
|
11
|
+
task :tag do
|
|
12
|
+
require_relative "lib/upright/version"
|
|
13
|
+
tag = "v#{Upright::VERSION}"
|
|
14
|
+
|
|
15
|
+
abort "tag: the working tree has uncommitted changes" unless `git status --porcelain`.empty?
|
|
16
|
+
|
|
17
|
+
branch = `git rev-parse --abbrev-ref HEAD`.strip
|
|
18
|
+
abort "tag: run this on main (currently on #{branch})" unless branch == "main"
|
|
19
|
+
|
|
20
|
+
sh "git fetch origin main --quiet"
|
|
21
|
+
abort "tag: HEAD differs from origin/main; pull or push first" unless `git rev-parse HEAD`.strip == `git rev-parse origin/main`.strip
|
|
22
|
+
abort "tag: #{tag} already exists locally" unless `git tag --list #{tag}`.strip.empty?
|
|
23
|
+
abort "tag: #{tag} already exists on origin" unless `git ls-remote --tags origin refs/tags/#{tag}`.strip.empty?
|
|
24
|
+
|
|
25
|
+
sh "git tag --annotate #{tag} --message 'upright #{tag}'"
|
|
26
|
+
sh "git push origin #{tag}"
|
|
27
|
+
puts "Pushed #{tag}. Approve the release-rubygems environment when the Release workflow pauses."
|
|
28
|
+
end
|
|
File without changes
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
|
|
36
36
|
--lch-green-dark: 55% 0.162 147;
|
|
37
37
|
--lch-red-dark: 59% 0.19 38;
|
|
38
|
+
--lch-orange-dark: 70% 0.17 65;
|
|
38
39
|
|
|
39
40
|
/* Colors: Semantic */
|
|
40
41
|
--color-black: oklch(var(--lch-black));
|
|
@@ -52,6 +53,7 @@
|
|
|
52
53
|
--color-link: oklch(var(--lch-blue-dark));
|
|
53
54
|
--color-positive: oklch(var(--lch-green-dark));
|
|
54
55
|
--color-negative: oklch(var(--lch-red-dark));
|
|
56
|
+
--color-warning: oklch(var(--lch-orange-dark));
|
|
55
57
|
--color-selected: oklch(var(--lch-blue-lighter));
|
|
56
58
|
|
|
57
59
|
/* Borders & Shadows */
|
|
@@ -22,17 +22,11 @@
|
|
|
22
22
|
|
|
23
23
|
.artifact__content {
|
|
24
24
|
background: var(--color-ink-lightest);
|
|
25
|
-
border-radius: 0.25rem;
|
|
26
|
-
box-shadow: var(--shadow);
|
|
27
25
|
display: flex;
|
|
28
26
|
flex-direction: column;
|
|
29
|
-
|
|
30
|
-
left: 50%;
|
|
27
|
+
inset: 0;
|
|
31
28
|
overflow: hidden;
|
|
32
29
|
position: fixed;
|
|
33
|
-
top: 50%;
|
|
34
|
-
transform: translate(-50%, -50%);
|
|
35
|
-
width: 80vw;
|
|
36
30
|
z-index: 1000;
|
|
37
31
|
}
|
|
38
32
|
|
|
@@ -123,26 +117,14 @@
|
|
|
123
117
|
}
|
|
124
118
|
}
|
|
125
119
|
|
|
126
|
-
|
|
127
|
-
|
|
120
|
+
.artifact__trace {
|
|
121
|
+
display: flex;
|
|
122
|
+
flex-direction: column;
|
|
123
|
+
gap: 0.5em;
|
|
128
124
|
}
|
|
129
125
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
width: calc(100vw - var(--block-space) * 2);
|
|
133
|
-
height: calc(100vh - var(--block-space) * 2);
|
|
134
|
-
left: 50%;
|
|
135
|
-
top: 50%;
|
|
136
|
-
}
|
|
126
|
+
td .artifact + .artifact {
|
|
127
|
+
margin-left: calc(var(--inline-space) / 2);
|
|
137
128
|
}
|
|
138
129
|
|
|
139
|
-
@media (max-width: 55ch) {
|
|
140
|
-
.artifact__content {
|
|
141
|
-
inset: 0;
|
|
142
|
-
width: 100%;
|
|
143
|
-
height: 100%;
|
|
144
|
-
transform: none;
|
|
145
|
-
border-radius: 0;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
130
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
.flash {
|
|
3
|
+
border-radius: 0.5rem;
|
|
4
|
+
color: var(--color-ink);
|
|
5
|
+
font-size: var(--text-small);
|
|
6
|
+
margin-block-end: var(--block-space);
|
|
7
|
+
padding: 0.75rem 1rem;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.flash--notice {
|
|
11
|
+
background: color-mix(in oklab, var(--color-positive) 12%, var(--color-canvas));
|
|
12
|
+
border: 1px solid color-mix(in oklab, var(--color-positive) 40%, var(--color-canvas));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.flash--alert {
|
|
16
|
+
background: color-mix(in oklab, var(--color-negative) 12%, var(--color-canvas));
|
|
17
|
+
border: 1px solid color-mix(in oklab, var(--color-negative) 40%, var(--color-canvas));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -38,6 +38,15 @@
|
|
|
38
38
|
text-transform: uppercase;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
.header__env {
|
|
42
|
+
color: var(--color-warning);
|
|
43
|
+
font-size: var(--text-x-small);
|
|
44
|
+
font-weight: 500;
|
|
45
|
+
letter-spacing: 0.06em;
|
|
46
|
+
margin-right: calc(var(--inline-space) * 2);
|
|
47
|
+
text-transform: uppercase;
|
|
48
|
+
}
|
|
49
|
+
|
|
41
50
|
.header__nav {
|
|
42
51
|
align-items: center;
|
|
43
52
|
display: flex;
|
|
@@ -62,6 +71,11 @@
|
|
|
62
71
|
&.active {
|
|
63
72
|
color: var(--color-positive);
|
|
64
73
|
}
|
|
74
|
+
|
|
75
|
+
&.unavailable {
|
|
76
|
+
color: var(--color-ink-light);
|
|
77
|
+
cursor: help;
|
|
78
|
+
}
|
|
65
79
|
}
|
|
66
80
|
|
|
67
81
|
.header__divider {
|