Compare commits
95 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a6208c0d49 | |||
| 7840fe66da | |||
| 2ca44c967e | |||
| 4b767421f3 | |||
| 6005b8609a | |||
| df23ecdf33 | |||
| f4988cbac5 | |||
| f4f5d16b4a | |||
| 1c4dd33381 | |||
| 9e0ba4d269 | |||
| d9ecf6c0d3 | |||
| 8051ad4dde | |||
| 165f98dc09 | |||
| ca7772250c | |||
| 6e02e4da02 | |||
| 9c8498cea7 | |||
| 965fbb08da | |||
| e16933eeac | |||
| 4d0fc0eae8 | |||
| 8296a973b8 | |||
| 19a9957755 | |||
| 02e3947906 | |||
| 766a73455e | |||
| 6e64ae09aa | |||
| 411eca20e0 | |||
| 0243d9e2fa | |||
| 9aa0e97be0 | |||
| 488fcfc820 | |||
| b5dad487e5 | |||
| 8b01187892 | |||
| d9d6ce0f30 | |||
| 8d203b3547 | |||
| fe5dbcff1e | |||
| 99df104cdd | |||
| a53397210c | |||
| 2533d8d34f | |||
| af2523cfee | |||
| c6e1663f8a | |||
| ab83c389f7 | |||
| 6d22702864 | |||
| d78957353d | |||
| b208493af9 | |||
| 4aa1485246 | |||
| e1e1d321dd | |||
| 3971b37abc | |||
| cf1bd3ea6b | |||
| 9b901766e3 | |||
| e19ee78e70 | |||
| c7c55ab95c | |||
| d7ddf01ea0 | |||
| c539af1a67 | |||
| d93d24b52d | |||
| 5dbfad68ad | |||
| 92c4506cfa | |||
| fe80bed6bd | |||
| b6e69021b2 | |||
| 12e624a496 | |||
| e95b44c690 | |||
| 4ee947d55c | |||
| 21212c0a1d | |||
| d1376a2200 | |||
| 7d2daf4f6a | |||
| da4562d308 | |||
| f51de52ff7 | |||
| 987632df39 | |||
| 28a3c3e53f | |||
| 1bd86f5abd | |||
| 989fbc25f8 | |||
| 0f935ceb48 | |||
| f844a435fd | |||
| 3a970e7a27 | |||
| 307c2bcdef | |||
| d62928aaae | |||
| d08a1e3ef6 | |||
| 2292041f9f | |||
| 75e4bf1d6e | |||
| 97add04276 | |||
| 1423f55d78 | |||
| 46d0b70399 | |||
| 168ca802d1 | |||
| 8c07e91f39 | |||
| 7979950c3b | |||
| 9846ba13e0 | |||
| 83839f7faf | |||
| 85fa3b1f8f | |||
| 4190f9a633 | |||
| 743ce27d2e | |||
| 399a2450ff | |||
| 934f16f0a5 | |||
| 0aeb13c181 | |||
| 5899bf2026 | |||
| 3b137964fc | |||
| 1bfdd0043f | |||
| 999c12748c | |||
| 6f283fd736 |
@@ -1,4 +1,3 @@
|
||||
/dist
|
||||
/vendor
|
||||
/.idea
|
||||
/.github
|
||||
|
||||
@@ -22,20 +22,19 @@ See [/docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md](docs/TROUBLESHOOTING_DEVICE_COLL
|
||||
|
||||
```
|
||||
docker run -it --rm -p 8080:8080 \
|
||||
-v `pwd`/config:/opt/scrutiny/config \
|
||||
-v /run/udev:/run/udev:ro \
|
||||
--cap-add SYS_RAWIO \
|
||||
--device=/dev/sda \
|
||||
--device=/dev/sdb \
|
||||
-e DEBUG=true \
|
||||
-e COLLECTOR_LOG_FILE=/tmp/collector.log \
|
||||
-e SCRUTINY_LOG_FILE=/tmp/web.log \
|
||||
-e COLLECTOR_LOG_FILE=/opt/scrutiny/config/collector.log \
|
||||
-e SCRUTINY_LOG_FILE=/opt/scrutiny/config/web.log \
|
||||
--name scrutiny \
|
||||
ghcr.io/analogj/scrutiny:master-omnibus
|
||||
|
||||
# in another terminal trigger the collector
|
||||
docker exec scrutiny scrutiny-collector-metrics run
|
||||
|
||||
# then use docker cp to copy the log files out of the container.
|
||||
docker cp scrutiny:/tmp/collector.log collector.log
|
||||
docker cp scrutiny:/tmp/web.log web.log
|
||||
```
|
||||
|
||||
The log files will be available on your host in the `config` directory. Please attach them to this issue.
|
||||
@@ -20,9 +20,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
with:
|
||||
platforms: 'arm64,arm'
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
# Login against a Docker registry except on PR
|
||||
@@ -40,6 +44,8 @@ jobs:
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
flavor: |
|
||||
latest=false
|
||||
tags: |
|
||||
type=ref,enable=true,event=branch,suffix=-collector
|
||||
type=ref,enable=true,event=tag,suffix=-collector
|
||||
@@ -56,8 +62,8 @@ jobs:
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
# cache-from: type=gha
|
||||
# cache-to: type=gha,mode=max
|
||||
|
||||
web:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -68,8 +74,22 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
- name: "Populate frontend version information"
|
||||
run: "cd webapp/frontend && ./git.version.sh"
|
||||
- name: "Generate frontend"
|
||||
uses: addnab/docker-run-action@v3
|
||||
with:
|
||||
image: node:lts
|
||||
options: -v ${{ github.workspace }}:/work
|
||||
run: |
|
||||
cd /work
|
||||
make frontend
|
||||
ls -alt /work
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
with:
|
||||
platforms: 'arm64,arm'
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
# Login against a Docker registry except on PR
|
||||
@@ -87,11 +107,12 @@ jobs:
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
flavor: |
|
||||
latest=false
|
||||
tags: |
|
||||
type=ref,enable=true,event=branch,suffix=-web
|
||||
type=ref,enable=true,event=tag,suffix=-web
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
# Build and push Docker image with Buildx (don't push on PR)
|
||||
# https://github.com/docker/build-push-action
|
||||
- name: Build and push Docker image
|
||||
@@ -103,8 +124,8 @@ jobs:
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
# cache-from: type=gha
|
||||
# cache-to: type=gha,mode=max
|
||||
omnibus:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
@@ -114,8 +135,22 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
- name: "Populate frontend version information"
|
||||
run: "cd webapp/frontend && ./git.version.sh"
|
||||
- name: "Generate frontend & version information"
|
||||
uses: addnab/docker-run-action@v3
|
||||
with:
|
||||
image: node:lts
|
||||
options: -v ${{ github.workspace }}:/work
|
||||
run: |
|
||||
cd /work
|
||||
make frontend
|
||||
ls -alt /work
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
with:
|
||||
platforms: 'arm64,arm'
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
# Login against a Docker registry except on PR
|
||||
@@ -137,7 +172,6 @@ jobs:
|
||||
type=ref,enable=true,event=branch,suffix=-omnibus
|
||||
type=ref,enable=true,event=tag,suffix=-omnibus
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
# Build and push Docker image with Buildx (don't push on PR)
|
||||
# https://github.com/docker/build-push-action
|
||||
- name: Build and push Docker image
|
||||
@@ -149,5 +183,5 @@ jobs:
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
# cache-from: type=gha
|
||||
# cache-to: type=gha,mode=max
|
||||
@@ -15,13 +15,15 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{github.event.release.tag_name}}
|
||||
- name: "Generate frontend version information"
|
||||
run: "cd webapp/frontend && ./git.version.sh"
|
||||
- name: Build Frontend
|
||||
run: |
|
||||
cd webapp/frontend
|
||||
npm install -g @angular/cli@9.1.4
|
||||
npm install
|
||||
mkdir -p dist
|
||||
ng build --output-path=dist --deploy-url="/web/" --base-href="/web/" --prod
|
||||
npm run build:prod -- --output-path=dist
|
||||
tar -czf scrutiny-web-frontend.tar.gz dist
|
||||
- name: Upload Frontend Asset
|
||||
id: upload-release-asset3
|
||||
|
||||
+125
-71
@@ -1,75 +1,109 @@
|
||||
# Contributing
|
||||
|
||||
There are multiple ways to develop on the scrutiny codebase locally. The two most popular are:
|
||||
- Docker Development Container - only requires docker
|
||||
- Run Components Locally - requires smartmontools, golang & nodejs installed locally
|
||||
The Scrutiny repository is a [monorepo](https://en.wikipedia.org/wiki/Monorepo) containing source code for:
|
||||
- Scrutiny Backend Server (API)
|
||||
- Scrutiny Frontend Angular SPA
|
||||
- S.M.A.R.T Collector
|
||||
|
||||
## Docker Development
|
||||
```
|
||||
docker build -f docker/Dockerfile . -t chcr.io/analogj/scrutiny:master-omnibus
|
||||
docker run -it --rm -p 8080:8080 \
|
||||
-v /run/udev:/run/udev:ro \
|
||||
--cap-add SYS_RAWIO \
|
||||
--device=/dev/sda \
|
||||
--device=/dev/sdb \
|
||||
ghcr.io/analogj/scrutiny:master-omnibus
|
||||
/opt/scrutiny/bin/scrutiny-collector-metrics run
|
||||
```
|
||||
Depending on the functionality you are adding, you may need to setup a development environment for 1 or more projects.
|
||||
|
||||
# Modifying the Scrutiny Backend Server (API)
|
||||
|
||||
## Local Development
|
||||
1. install the [Go runtime](https://go.dev/doc/install) (v1.17+)
|
||||
2. download the `scrutiny-web-frontend.tar.gz` for the [latest release](https://github.com/AnalogJ/scrutiny/releases/latest). Extract to a folder named `dist`
|
||||
3. create a `scrutiny.yaml` config file
|
||||
```yaml
|
||||
# config file for local development. store as scrutiny.yaml
|
||||
version: 1
|
||||
|
||||
web:
|
||||
listen:
|
||||
port: 8080
|
||||
host: 0.0.0.0
|
||||
database:
|
||||
# can also set absolute path here
|
||||
location: ./scrutiny.db
|
||||
src:
|
||||
frontend:
|
||||
path: ./dist
|
||||
influxdb:
|
||||
retention_policy: false
|
||||
|
||||
log:
|
||||
file: 'web.log' #absolute or relative paths allowed, eg. web.log
|
||||
level: DEBUG
|
||||
|
||||
### Frontend
|
||||
The frontend is written in Angular.
|
||||
If you're working on the frontend and can use mocked data rather than a real backend, you can use
|
||||
```
|
||||
cd webapp/frontend
|
||||
npm install
|
||||
ng serve --deploy-url="/web/" --base-href="/web/"
|
||||
```
|
||||
```
|
||||
4. start a InfluxDB docker container.
|
||||
```bash
|
||||
docker run -p 8086:8086 --rm influxdb:2.2
|
||||
```
|
||||
5. start the scrutiny web server
|
||||
```bash
|
||||
go mod vendor
|
||||
go run webapp/backend/cmd/scrutiny/scrutiny.go start --config ./scrutiny.yaml
|
||||
```
|
||||
6. open your browser to [http://localhost:8080/web](http://localhost:8080/web)
|
||||
|
||||
However, if you need to also run the backend, and use real data, you'll need to run the following command:
|
||||
```
|
||||
cd webapp/frontend && ng build --watch --output-path=../../dist --prod
|
||||
```
|
||||
# Modifying the Scrutiny Frontend Angular SPA
|
||||
|
||||
> Note: if you do not add `--prod` flag, app will display mocked data for api calls.
|
||||
The frontend is written in Angular. If you're working on the frontend and can use mocked data rather than a real backend, you can follow the instructions below:
|
||||
|
||||
### Backend
|
||||
1. install [NodeJS](https://nodejs.org/en/download/)
|
||||
2. start the Angular Frontend Application
|
||||
```bash
|
||||
cd webapp/frontend
|
||||
npm install
|
||||
npm run start -- --deploy-url="/web/" --base-href="/web/" --port 4200
|
||||
```
|
||||
3. open your browser and visit [http://localhost:4200/web](http://localhost:4200/web)
|
||||
|
||||
If you're using the `ng build` command above to generate your frontend, you'll need to create a custom config file and
|
||||
override the `web.src.frontend.path` value.
|
||||
# Modifying both Scrutiny Backend and Frontend Applications
|
||||
If you're developing a feature that requires changes to the backend and the frontend, or a frontend feature that requires real data,
|
||||
you'll need to follow the steps below:
|
||||
|
||||
```
|
||||
# config file for local development. store as scrutiny.yaml
|
||||
version: 1
|
||||
1. install the [Go runtime](https://go.dev/doc/install) (v1.17+)
|
||||
2. install [NodeJS](https://nodejs.org/en/download/)
|
||||
3. create a `scrutiny.yaml` config file
|
||||
```yaml
|
||||
# config file for local development. store as scrutiny.yaml
|
||||
version: 1
|
||||
|
||||
web:
|
||||
listen:
|
||||
port: 8080
|
||||
host: 0.0.0.0
|
||||
database:
|
||||
# can also set absolute path here
|
||||
location: ./scrutiny.db
|
||||
src:
|
||||
frontend:
|
||||
path: ./dist
|
||||
influxdb:
|
||||
retention_policy: false
|
||||
|
||||
log:
|
||||
file: 'web.log' #absolute or relative paths allowed, eg. web.log
|
||||
level: DEBUG
|
||||
|
||||
web:
|
||||
listen:
|
||||
port: 8080
|
||||
host: 0.0.0.0
|
||||
database:
|
||||
# can also set absolute path here
|
||||
location: ./scrutiny.db
|
||||
src:
|
||||
frontend:
|
||||
path: ./dist
|
||||
influxdb:
|
||||
retention_policy: false
|
||||
|
||||
log:
|
||||
file: 'web.log' #absolute or relative paths allowed, eg. web.log
|
||||
level: DEBUG
|
||||
|
||||
```
|
||||
|
||||
Once you've created a config file, you can pass it to the scrutiny binary during startup.
|
||||
|
||||
```
|
||||
go run webapp/backend/cmd/scrutiny/scrutiny.go start --config ./scrutiny.yaml
|
||||
```
|
||||
|
||||
Now visit http://localhost:8080
|
||||
```
|
||||
4. start a InfluxDB docker container.
|
||||
```bash
|
||||
docker run -p 8086:8086 --rm influxdb:2.2
|
||||
```
|
||||
5. build the Angular Frontend Application
|
||||
```bash
|
||||
cd webapp/frontend
|
||||
npm install
|
||||
npm run build:prod -- --watch --output-path=../../dist
|
||||
# Note: if you do not add `--prod` flag, app will display mocked data for api calls.
|
||||
```
|
||||
6. start the scrutiny web server
|
||||
```bash
|
||||
go mod vendor
|
||||
go run webapp/backend/cmd/scrutiny/scrutiny.go start --config ./scrutiny.yaml
|
||||
```
|
||||
7. open your browser to [http://localhost:8080/web](http://localhost:8080/web)
|
||||
|
||||
|
||||
If you'd like to populate the database with some test data, you can run the following commands:
|
||||
@@ -82,15 +116,6 @@ If you'd like to populate the database with some test data, you can run the fol
|
||||
docker run -p 8086:8086 --rm influxdb:2.2
|
||||
|
||||
|
||||
docker run --rm -p 8086:8086 \
|
||||
-e DOCKER_INFLUXDB_INIT_MODE=setup \
|
||||
-e DOCKER_INFLUXDB_INIT_USERNAME=admin \
|
||||
-e DOCKER_INFLUXDB_INIT_PASSWORD=password12345 \
|
||||
-e DOCKER_INFLUXDB_INIT_ORG=scrutiny \
|
||||
-e DOCKER_INFLUXDB_INIT_BUCKET=metrics \
|
||||
influxdb:2.2
|
||||
|
||||
|
||||
# curl -X POST -H "Content-Type: application/json" -d @webapp/backend/pkg/web/testdata/register-devices-req.json localhost:8080/api/devices/register
|
||||
# curl -X POST -H "Content-Type: application/json" -d @webapp/backend/pkg/models/testdata/smart-ata.json localhost:8080/api/device/0x5000cca264eb01d7/smart
|
||||
# curl -X POST -H "Content-Type: application/json" -d @webapp/backend/pkg/models/testdata/smart-ata-date.json localhost:8080/api/device/0x5000cca264eb01d7/smart
|
||||
@@ -105,14 +130,14 @@ curl localhost:8080/api/summary
|
||||
|
||||
```
|
||||
|
||||
### Collector
|
||||
# Modifying the Collector
|
||||
```
|
||||
brew install smartmontools
|
||||
go run collector/cmd/collector-metrics/collector-metrics.go run --debug
|
||||
```
|
||||
|
||||
|
||||
## Debugging
|
||||
# Debugging
|
||||
|
||||
If you need more verbose logs for debugging, you can use the following environmental variables:
|
||||
|
||||
@@ -131,3 +156,32 @@ Finally, you can copy the files from the scrutiny container to your host using t
|
||||
docker cp scrutiny:/tmp/collector.log collector.log
|
||||
docker cp scrutiny:/tmp/web.log web.log
|
||||
```
|
||||
|
||||
# Docker Development
|
||||
|
||||
```
|
||||
docker build -f docker/Dockerfile . -t chcr.io/analogj/scrutiny:master-omnibus
|
||||
docker run -it --rm -p 8080:8080 \
|
||||
-v /run/udev:/run/udev:ro \
|
||||
--cap-add SYS_RAWIO \
|
||||
--device=/dev/sda \
|
||||
--device=/dev/sdb \
|
||||
ghcr.io/analogj/scrutiny:master-omnibus
|
||||
/opt/scrutiny/bin/scrutiny-collector-metrics run
|
||||
```
|
||||
|
||||
|
||||
# Running Tests
|
||||
|
||||
```bash
|
||||
docker run -p 8086:8086 -d --rm \
|
||||
-e DOCKER_INFLUXDB_INIT_MODE=setup \
|
||||
-e DOCKER_INFLUXDB_INIT_USERNAME=admin \
|
||||
-e DOCKER_INFLUXDB_INIT_PASSWORD=password12345 \
|
||||
-e DOCKER_INFLUXDB_INIT_ORG=scrutiny \
|
||||
-e DOCKER_INFLUXDB_INIT_BUCKET=metrics \
|
||||
-e DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=my-super-secret-auth-token \
|
||||
influxdb:2.2
|
||||
go test ./...
|
||||
|
||||
```
|
||||
@@ -9,6 +9,7 @@ BINARY=\
|
||||
linux/arm-7 \
|
||||
linux/arm64 \
|
||||
|
||||
.ONESHELL: # Applies to every targets in the file! .ONESHELL instructs make to invoke a single instance of the shell and provide it with the entire recipe, regardless of how many lines it contains.
|
||||
.PHONY: all $(BINARY)
|
||||
all: $(BINARY) windows/amd64
|
||||
|
||||
@@ -38,5 +39,28 @@ windows/amd64:
|
||||
@echo "building collector binary (OS = $(OS), ARCH = $(ARCH))"
|
||||
xgo -v --targets="$(OS)/$(ARCH)" -ldflags "-extldflags=-static -X main.goos=$(OS) -X main.goarch=$(ARCH)" -out scrutiny-collector-metrics -tags "static netgo" ${GO_WORKSPACE}/collector/cmd/collector-metrics/
|
||||
|
||||
|
||||
docker-collector:
|
||||
@echo "building collector docker image"
|
||||
docker build --build-arg TARGETARCH=amd64 -f docker/Dockerfile.collector -t analogj/scrutiny-dev:collector .
|
||||
|
||||
docker-web:
|
||||
@echo "building web docker image"
|
||||
docker build --build-arg TARGETARCH=amd64 -f docker/Dockerfile.web -t analogj/scrutiny-dev:web .
|
||||
|
||||
docker-omnibus:
|
||||
@echo "building omnibus docker image"
|
||||
docker build --build-arg TARGETARCH=amd64 -f docker/Dockerfile -t analogj/scrutiny-dev:omnibus .
|
||||
|
||||
# reduce logging, disable angular-cli analytics for ci environment
|
||||
frontend: export NPM_CONFIG_LOGLEVEL = warn
|
||||
frontend: export NG_CLI_ANALYTICS = false
|
||||
frontend:
|
||||
cd webapp/frontend
|
||||
npm install -g @angular/cli@9.1.4
|
||||
mkdir -p $(CURDIR)/dist
|
||||
npm install
|
||||
npm run build:prod -- --output-path=$(CURDIR)/dist
|
||||
|
||||
# clean:
|
||||
# rm scrutiny-collector-metrics-* scrutiny-web-*
|
||||
|
||||
@@ -17,8 +17,6 @@
|
||||
WebUI for smartd S.M.A.R.T monitoring
|
||||
|
||||
> NOTE: Scrutiny is a Work-in-Progress and still has some rough edges.
|
||||
>
|
||||
> WARNING: Once the [InfluxDB](https://github.com/AnalogJ/scrutiny/tree/influxdb) branch is merged, Scrutiny will use both sqlite and InfluxDB for data storage. Unfortunately, this may not be backwards compatible with the database structures in the master (sqlite only) branch.
|
||||
|
||||
[](https://imgur.com/a/5k8qMzS)
|
||||
|
||||
@@ -60,11 +58,12 @@ Scrutiny uses `smartctl --scan` to detect devices/drives.
|
||||
- All RAID controllers supported by `smartctl` are automatically supported by Scrutiny.
|
||||
- While some RAID controllers support passing through the underlying SMART data to `smartctl` others do not.
|
||||
- In some cases `--scan` does not correctly detect the device type, returning [incomplete SMART data](https://github.com/AnalogJ/scrutiny/issues/45).
|
||||
Scrutiny will eventually support overriding detected device type via the config file.
|
||||
Scrutiny supports overriding detected device type via the config file: see [example.collector.yaml](https://github.com/AnalogJ/scrutiny/blob/master/example.collector.yaml)
|
||||
- If you use docker, you **must** pass though the RAID virtual disk to the container using `--device` (see below)
|
||||
- This device may be in `/dev/*` or `/dev/bus/*`.
|
||||
- If you're unsure, run `smartctl --scan` on your host, and pass all listed devices to the container.
|
||||
|
||||
See [docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md](./docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md) for help
|
||||
|
||||
## Docker
|
||||
|
||||
@@ -178,7 +177,9 @@ Scrutiny supports sending SMART device failure notifications via the following s
|
||||
- Telegram
|
||||
- Tulip
|
||||
|
||||
Check the `notify.urls` section of [example.scrutiny.yml](example.scrutiny.yaml) for more information and documentation for service specific setup.
|
||||
Check the `notify.urls` section of [example.scrutiny.yml](example.scrutiny.yaml) for examples.
|
||||
|
||||
For more information and troubleshooting, see the [TROUBLESHOOTING_NOTIFICATIONS.md](./docs/TROUBLESHOOTING_NOTIFICATIONS.md) file
|
||||
|
||||
### Testing Notifications
|
||||
|
||||
@@ -239,7 +240,7 @@ scrutiny-collector-metrics run --debug --log-file /tmp/collector.log
|
||||
| arm-6 | :white_check_mark: | |
|
||||
| arm-7 | :white_check_mark: | web/collector only. see [#236](https://github.com/AnalogJ/scrutiny/issues/236) |
|
||||
| arm64 | :white_check_mark: | :white_check_mark: |
|
||||
| freebsd | :white_check_mark: | |
|
||||
| freebsd | collector only. see [#238](https://github.com/AnalogJ/scrutiny/issues/238) | |
|
||||
| macos-amd64 | | :white_check_mark: |
|
||||
| macos-arm64 | | :white_check_mark: |
|
||||
| windows-amd64 | :white_check_mark: | |
|
||||
|
||||
@@ -116,12 +116,13 @@ func (mc *MetricsCollector) Collect(deviceWWN string, deviceName string, deviceT
|
||||
}
|
||||
mc.logger.Infof("Collecting smartctl results for %s\n", deviceName)
|
||||
|
||||
args := []string{"-x", "-j"}
|
||||
fullDeviceName := fmt.Sprintf("%s%s", detect.DevicePrefix(), deviceName)
|
||||
args := strings.Split(mc.config.GetCommandMetricsSmartArgs(fullDeviceName), " ")
|
||||
//only include the device type if its a non-standard one. In some cases ata drives are detected as scsi in docker, and metadata is lost.
|
||||
if len(deviceType) > 0 && deviceType != "scsi" && deviceType != "ata" {
|
||||
args = append(args, "-d", deviceType)
|
||||
args = append(args, "--device", deviceType)
|
||||
}
|
||||
args = append(args, fmt.Sprintf("%s%s", detect.DevicePrefix(), deviceName))
|
||||
args = append(args, fullDeviceName)
|
||||
|
||||
result, err := mc.shell.Command(mc.logger, "smartctl", args, "", os.Environ())
|
||||
resultBytes := []byte(result)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/analogj/go-util/utils"
|
||||
"github.com/analogj/scrutiny/collector/pkg/errors"
|
||||
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||
@@ -8,6 +9,8 @@ import (
|
||||
"github.com/spf13/viper"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// When initializing this class the following methods must be called:
|
||||
@@ -16,6 +19,8 @@ import (
|
||||
// This is done automatically when created via the Factory.
|
||||
type configuration struct {
|
||||
*viper.Viper
|
||||
|
||||
deviceOverrides []models.ScanOverride
|
||||
}
|
||||
|
||||
//Viper uses the following precedence order. Each item takes precedence over the item below it:
|
||||
@@ -38,6 +43,10 @@ func (c *configuration) Init() error {
|
||||
|
||||
c.SetDefault("api.endpoint", "http://localhost:8080")
|
||||
|
||||
c.SetDefault("commands.metrics_scan_args", "--scan --json")
|
||||
c.SetDefault("commands.metrics_info_args", "--info --json")
|
||||
c.SetDefault("commands.metrics_smart_args", "--xall --json")
|
||||
|
||||
//c.SetDefault("collect.short.command", "-a -o on -S on")
|
||||
|
||||
//if you want to load a non-standard location system config file (~/drawbridge.yml), use ReadConfig
|
||||
@@ -90,16 +99,89 @@ func (c *configuration) ValidateConfig() error {
|
||||
// check that device prefix matches OS
|
||||
// check that schema of config file is valid
|
||||
|
||||
return nil
|
||||
// check that the collector commands are valid
|
||||
commandArgStrings := map[string]string{
|
||||
"commands.metrics_scan_args": c.GetString("commands.metrics_scan_args"),
|
||||
"commands.metrics_info_args": c.GetString("commands.metrics_info_args"),
|
||||
"commands.metrics_smart_args": c.GetString("commands.metrics_smart_args"),
|
||||
}
|
||||
|
||||
errorStrings := []string{}
|
||||
for configKey, commandArgString := range commandArgStrings {
|
||||
args := strings.Split(commandArgString, " ")
|
||||
//ensure that the args string contains `--json` or `-j` flag
|
||||
containsJsonFlag := false
|
||||
containsDeviceFlag := false
|
||||
for _, flag := range args {
|
||||
if strings.HasPrefix(flag, "--json") || strings.HasPrefix(flag, "-j") {
|
||||
containsJsonFlag = true
|
||||
}
|
||||
if strings.HasPrefix(flag, "--device") || strings.HasPrefix(flag, "-d") {
|
||||
containsDeviceFlag = true
|
||||
}
|
||||
}
|
||||
|
||||
if !containsJsonFlag {
|
||||
errorStrings = append(errorStrings, fmt.Sprintf("configuration key '%s' is missing '--json' flag", configKey))
|
||||
}
|
||||
|
||||
if containsDeviceFlag {
|
||||
errorStrings = append(errorStrings, fmt.Sprintf("configuration key '%s' must not contain '--device' or '-d' flag", configKey))
|
||||
}
|
||||
}
|
||||
//sort(errorStrings)
|
||||
sort.Strings(errorStrings)
|
||||
|
||||
if len(errorStrings) == 0 {
|
||||
return nil
|
||||
} else {
|
||||
return errors.ConfigValidationError(strings.Join(errorStrings, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *configuration) GetScanOverrides() []models.ScanOverride {
|
||||
func (c *configuration) GetDeviceOverrides() []models.ScanOverride {
|
||||
// we have to support 2 types of device types.
|
||||
// - simple device type (device_type: 'sat')
|
||||
// and list of device types (type: \n- 3ware,0 \n- 3ware,1 \n- 3ware,2)
|
||||
// GetString will return "" if this is a list of device types.
|
||||
|
||||
overrides := []models.ScanOverride{}
|
||||
c.UnmarshalKey("devices", &overrides, func(c *mapstructure.DecoderConfig) { c.WeaklyTypedInput = true })
|
||||
return overrides
|
||||
if c.deviceOverrides == nil {
|
||||
overrides := []models.ScanOverride{}
|
||||
c.UnmarshalKey("devices", &overrides, func(c *mapstructure.DecoderConfig) { c.WeaklyTypedInput = true })
|
||||
c.deviceOverrides = overrides
|
||||
}
|
||||
|
||||
return c.deviceOverrides
|
||||
}
|
||||
|
||||
func (c *configuration) GetCommandMetricsInfoArgs(deviceName string) string {
|
||||
overrides := c.GetDeviceOverrides()
|
||||
|
||||
for _, deviceOverrides := range overrides {
|
||||
if strings.ToLower(deviceName) == strings.ToLower(deviceOverrides.Device) {
|
||||
//found matching device
|
||||
if len(deviceOverrides.Commands.MetricsInfoArgs) > 0 {
|
||||
return deviceOverrides.Commands.MetricsInfoArgs
|
||||
} else {
|
||||
return c.GetString("commands.metrics_info_args")
|
||||
}
|
||||
}
|
||||
}
|
||||
return c.GetString("commands.metrics_info_args")
|
||||
}
|
||||
|
||||
func (c *configuration) GetCommandMetricsSmartArgs(deviceName string) string {
|
||||
overrides := c.GetDeviceOverrides()
|
||||
|
||||
for _, deviceOverrides := range overrides {
|
||||
if strings.ToLower(deviceName) == strings.ToLower(deviceOverrides.Device) {
|
||||
//found matching device
|
||||
if len(deviceOverrides.Commands.MetricsSmartArgs) > 0 {
|
||||
return deviceOverrides.Commands.MetricsSmartArgs
|
||||
} else {
|
||||
return c.GetString("commands.metrics_smart_args")
|
||||
}
|
||||
}
|
||||
}
|
||||
return c.GetString("commands.metrics_smart_args")
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ func TestConfiguration_GetScanOverrides_Simple(t *testing.T) {
|
||||
//test
|
||||
err := testConfig.ReadConfig(path.Join("testdata", "simple_device.yaml"))
|
||||
require.NoError(t, err, "should correctly load simple device config")
|
||||
scanOverrides := testConfig.GetScanOverrides()
|
||||
scanOverrides := testConfig.GetDeviceOverrides()
|
||||
|
||||
//assert
|
||||
require.Equal(t, []models.ScanOverride{{Device: "/dev/sda", DeviceType: []string{"sat"}, Ignore: false}}, scanOverrides)
|
||||
@@ -45,7 +45,7 @@ func TestConfiguration_GetScanOverrides_Ignore(t *testing.T) {
|
||||
//test
|
||||
err := testConfig.ReadConfig(path.Join("testdata", "ignore_device.yaml"))
|
||||
require.NoError(t, err, "should correctly load ignore device config")
|
||||
scanOverrides := testConfig.GetScanOverrides()
|
||||
scanOverrides := testConfig.GetDeviceOverrides()
|
||||
|
||||
//assert
|
||||
require.Equal(t, []models.ScanOverride{{Device: "/dev/sda", DeviceType: nil, Ignore: true}}, scanOverrides)
|
||||
@@ -60,7 +60,7 @@ func TestConfiguration_GetScanOverrides_Raid(t *testing.T) {
|
||||
//test
|
||||
err := testConfig.ReadConfig(path.Join("testdata", "raid_device.yaml"))
|
||||
require.NoError(t, err, "should correctly load ignore device config")
|
||||
scanOverrides := testConfig.GetScanOverrides()
|
||||
scanOverrides := testConfig.GetDeviceOverrides()
|
||||
|
||||
//assert
|
||||
require.Equal(t, []models.ScanOverride{
|
||||
@@ -75,3 +75,53 @@ func TestConfiguration_GetScanOverrides_Raid(t *testing.T) {
|
||||
Ignore: false,
|
||||
}}, scanOverrides)
|
||||
}
|
||||
|
||||
func TestConfiguration_InvalidCommands_MissingJson(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
//setup
|
||||
testConfig, _ := config.Create()
|
||||
|
||||
//test
|
||||
err := testConfig.ReadConfig(path.Join("testdata", "invalid_commands_missing_json.yaml"))
|
||||
require.EqualError(t, err, `ConfigValidationError: "configuration key 'commands.metrics_scan_args' is missing '--json' flag"`, "should throw an error because json flag is missing")
|
||||
}
|
||||
|
||||
func TestConfiguration_InvalidCommands_IncludesDevice(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
//setup
|
||||
testConfig, _ := config.Create()
|
||||
|
||||
//test
|
||||
err := testConfig.ReadConfig(path.Join("testdata", "invalid_commands_includes_device.yaml"))
|
||||
require.EqualError(t, err, `ConfigValidationError: "configuration key 'commands.metrics_info_args' must not contain '--device' or '-d' flag, configuration key 'commands.metrics_smart_args' must not contain '--device' or '-d' flag"`, "should throw an error because device flags detected")
|
||||
}
|
||||
|
||||
func TestConfiguration_OverrideCommands(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
//setup
|
||||
testConfig, _ := config.Create()
|
||||
|
||||
//test
|
||||
err := testConfig.ReadConfig(path.Join("testdata", "override_commands.yaml"))
|
||||
require.NoError(t, err, "should not throw an error")
|
||||
require.Equal(t, "--xall --json -T permissive", testConfig.GetString("commands.metrics_smart_args"))
|
||||
}
|
||||
|
||||
func TestConfiguration_OverrideDeviceCommands_MetricsInfoArgs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
//setup
|
||||
testConfig, _ := config.Create()
|
||||
|
||||
//test
|
||||
err := testConfig.ReadConfig(path.Join("testdata", "override_device_commands.yaml"))
|
||||
require.NoError(t, err, "should correctly override device command")
|
||||
|
||||
//assert
|
||||
require.Equal(t, "--info --json -T permissive", testConfig.GetCommandMetricsInfoArgs("/dev/sda"))
|
||||
require.Equal(t, "--info --json", testConfig.GetCommandMetricsInfoArgs("/dev/sdb"))
|
||||
//require.Equal(t, []models.ScanOverride{{Device: "/dev/sda", DeviceType: nil, Commands: {MetricsInfoArgs: "--info --json -T "}}}, scanOverrides)
|
||||
}
|
||||
|
||||
@@ -22,5 +22,7 @@ type Interface interface {
|
||||
GetStringSlice(key string) []string
|
||||
UnmarshalKey(key string, rawVal interface{}, decoderOpts ...viper.DecoderConfigOption) error
|
||||
|
||||
GetScanOverrides() []models.ScanOverride
|
||||
GetDeviceOverrides() []models.ScanOverride
|
||||
GetCommandMetricsInfoArgs(deviceName string) string
|
||||
GetCommandMetricsSmartArgs(deviceName string) string
|
||||
}
|
||||
|
||||
@@ -5,88 +5,37 @@
|
||||
package mock_config
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
models "github.com/analogj/scrutiny/collector/pkg/models"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
viper "github.com/spf13/viper"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockInterface is a mock of Interface interface
|
||||
// MockInterface is a mock of Interface interface.
|
||||
type MockInterface struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockInterfaceMockRecorder
|
||||
}
|
||||
|
||||
// MockInterfaceMockRecorder is the mock recorder for MockInterface
|
||||
// MockInterfaceMockRecorder is the mock recorder for MockInterface.
|
||||
type MockInterfaceMockRecorder struct {
|
||||
mock *MockInterface
|
||||
}
|
||||
|
||||
// NewMockInterface creates a new mock instance
|
||||
// NewMockInterface creates a new mock instance.
|
||||
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
|
||||
mock := &MockInterface{ctrl: ctrl}
|
||||
mock.recorder = &MockInterfaceMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Init mocks base method
|
||||
func (m *MockInterface) Init() error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Init")
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Init indicates an expected call of Init
|
||||
func (mr *MockInterfaceMockRecorder) Init() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockInterface)(nil).Init))
|
||||
}
|
||||
|
||||
// ReadConfig mocks base method
|
||||
func (m *MockInterface) ReadConfig(configFilePath string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ReadConfig", configFilePath)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// ReadConfig indicates an expected call of ReadConfig
|
||||
func (mr *MockInterfaceMockRecorder) ReadConfig(configFilePath interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadConfig", reflect.TypeOf((*MockInterface)(nil).ReadConfig), configFilePath)
|
||||
}
|
||||
|
||||
// Set mocks base method
|
||||
func (m *MockInterface) Set(key string, value interface{}) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Set", key, value)
|
||||
}
|
||||
|
||||
// Set indicates an expected call of Set
|
||||
func (mr *MockInterfaceMockRecorder) Set(key, value interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockInterface)(nil).Set), key, value)
|
||||
}
|
||||
|
||||
// SetDefault mocks base method
|
||||
func (m *MockInterface) SetDefault(key string, value interface{}) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "SetDefault", key, value)
|
||||
}
|
||||
|
||||
// SetDefault indicates an expected call of SetDefault
|
||||
func (mr *MockInterfaceMockRecorder) SetDefault(key, value interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDefault", reflect.TypeOf((*MockInterface)(nil).SetDefault), key, value)
|
||||
}
|
||||
|
||||
// AllSettings mocks base method
|
||||
// AllSettings mocks base method.
|
||||
func (m *MockInterface) AllSettings() map[string]interface{} {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "AllSettings")
|
||||
@@ -94,27 +43,13 @@ func (m *MockInterface) AllSettings() map[string]interface{} {
|
||||
return ret0
|
||||
}
|
||||
|
||||
// AllSettings indicates an expected call of AllSettings
|
||||
// AllSettings indicates an expected call of AllSettings.
|
||||
func (mr *MockInterfaceMockRecorder) AllSettings() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AllSettings", reflect.TypeOf((*MockInterface)(nil).AllSettings))
|
||||
}
|
||||
|
||||
// IsSet mocks base method
|
||||
func (m *MockInterface) IsSet(key string) bool {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "IsSet", key)
|
||||
ret0, _ := ret[0].(bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// IsSet indicates an expected call of IsSet
|
||||
func (mr *MockInterfaceMockRecorder) IsSet(key interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsSet", reflect.TypeOf((*MockInterface)(nil).IsSet), key)
|
||||
}
|
||||
|
||||
// Get mocks base method
|
||||
// Get mocks base method.
|
||||
func (m *MockInterface) Get(key string) interface{} {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Get", key)
|
||||
@@ -122,13 +57,13 @@ func (m *MockInterface) Get(key string) interface{} {
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Get indicates an expected call of Get
|
||||
// Get indicates an expected call of Get.
|
||||
func (mr *MockInterfaceMockRecorder) Get(key interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockInterface)(nil).Get), key)
|
||||
}
|
||||
|
||||
// GetBool mocks base method
|
||||
// GetBool mocks base method.
|
||||
func (m *MockInterface) GetBool(key string) bool {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetBool", key)
|
||||
@@ -136,13 +71,55 @@ func (m *MockInterface) GetBool(key string) bool {
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetBool indicates an expected call of GetBool
|
||||
// GetBool indicates an expected call of GetBool.
|
||||
func (mr *MockInterfaceMockRecorder) GetBool(key interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBool", reflect.TypeOf((*MockInterface)(nil).GetBool), key)
|
||||
}
|
||||
|
||||
// GetInt mocks base method
|
||||
// GetCommandMetricsInfoArgs mocks base method.
|
||||
func (m *MockInterface) GetCommandMetricsInfoArgs(deviceName string) string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetCommandMetricsInfoArgs", deviceName)
|
||||
ret0, _ := ret[0].(string)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetCommandMetricsInfoArgs indicates an expected call of GetCommandMetricsInfoArgs.
|
||||
func (mr *MockInterfaceMockRecorder) GetCommandMetricsInfoArgs(deviceName interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCommandMetricsInfoArgs", reflect.TypeOf((*MockInterface)(nil).GetCommandMetricsInfoArgs), deviceName)
|
||||
}
|
||||
|
||||
// GetCommandMetricsSmartArgs mocks base method.
|
||||
func (m *MockInterface) GetCommandMetricsSmartArgs(deviceName string) string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetCommandMetricsSmartArgs", deviceName)
|
||||
ret0, _ := ret[0].(string)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetCommandMetricsSmartArgs indicates an expected call of GetCommandMetricsSmartArgs.
|
||||
func (mr *MockInterfaceMockRecorder) GetCommandMetricsSmartArgs(deviceName interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCommandMetricsSmartArgs", reflect.TypeOf((*MockInterface)(nil).GetCommandMetricsSmartArgs), deviceName)
|
||||
}
|
||||
|
||||
// GetDeviceOverrides mocks base method.
|
||||
func (m *MockInterface) GetDeviceOverrides() []models.ScanOverride {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetDeviceOverrides")
|
||||
ret0, _ := ret[0].([]models.ScanOverride)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetDeviceOverrides indicates an expected call of GetDeviceOverrides.
|
||||
func (mr *MockInterfaceMockRecorder) GetDeviceOverrides() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeviceOverrides", reflect.TypeOf((*MockInterface)(nil).GetDeviceOverrides))
|
||||
}
|
||||
|
||||
// GetInt mocks base method.
|
||||
func (m *MockInterface) GetInt(key string) int {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetInt", key)
|
||||
@@ -150,13 +127,13 @@ func (m *MockInterface) GetInt(key string) int {
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetInt indicates an expected call of GetInt
|
||||
// GetInt indicates an expected call of GetInt.
|
||||
func (mr *MockInterfaceMockRecorder) GetInt(key interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInt", reflect.TypeOf((*MockInterface)(nil).GetInt), key)
|
||||
}
|
||||
|
||||
// GetString mocks base method
|
||||
// GetString mocks base method.
|
||||
func (m *MockInterface) GetString(key string) string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetString", key)
|
||||
@@ -164,13 +141,13 @@ func (m *MockInterface) GetString(key string) string {
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetString indicates an expected call of GetString
|
||||
// GetString indicates an expected call of GetString.
|
||||
func (mr *MockInterfaceMockRecorder) GetString(key interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetString", reflect.TypeOf((*MockInterface)(nil).GetString), key)
|
||||
}
|
||||
|
||||
// GetStringSlice mocks base method
|
||||
// GetStringSlice mocks base method.
|
||||
func (m *MockInterface) GetStringSlice(key string) []string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetStringSlice", key)
|
||||
@@ -178,13 +155,79 @@ func (m *MockInterface) GetStringSlice(key string) []string {
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetStringSlice indicates an expected call of GetStringSlice
|
||||
// GetStringSlice indicates an expected call of GetStringSlice.
|
||||
func (mr *MockInterfaceMockRecorder) GetStringSlice(key interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStringSlice", reflect.TypeOf((*MockInterface)(nil).GetStringSlice), key)
|
||||
}
|
||||
|
||||
// UnmarshalKey mocks base method
|
||||
// Init mocks base method.
|
||||
func (m *MockInterface) Init() error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Init")
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Init indicates an expected call of Init.
|
||||
func (mr *MockInterfaceMockRecorder) Init() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockInterface)(nil).Init))
|
||||
}
|
||||
|
||||
// IsSet mocks base method.
|
||||
func (m *MockInterface) IsSet(key string) bool {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "IsSet", key)
|
||||
ret0, _ := ret[0].(bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// IsSet indicates an expected call of IsSet.
|
||||
func (mr *MockInterfaceMockRecorder) IsSet(key interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsSet", reflect.TypeOf((*MockInterface)(nil).IsSet), key)
|
||||
}
|
||||
|
||||
// ReadConfig mocks base method.
|
||||
func (m *MockInterface) ReadConfig(configFilePath string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ReadConfig", configFilePath)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// ReadConfig indicates an expected call of ReadConfig.
|
||||
func (mr *MockInterfaceMockRecorder) ReadConfig(configFilePath interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadConfig", reflect.TypeOf((*MockInterface)(nil).ReadConfig), configFilePath)
|
||||
}
|
||||
|
||||
// Set mocks base method.
|
||||
func (m *MockInterface) Set(key string, value interface{}) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Set", key, value)
|
||||
}
|
||||
|
||||
// Set indicates an expected call of Set.
|
||||
func (mr *MockInterfaceMockRecorder) Set(key, value interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockInterface)(nil).Set), key, value)
|
||||
}
|
||||
|
||||
// SetDefault mocks base method.
|
||||
func (m *MockInterface) SetDefault(key string, value interface{}) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "SetDefault", key, value)
|
||||
}
|
||||
|
||||
// SetDefault indicates an expected call of SetDefault.
|
||||
func (mr *MockInterfaceMockRecorder) SetDefault(key, value interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDefault", reflect.TypeOf((*MockInterface)(nil).SetDefault), key, value)
|
||||
}
|
||||
|
||||
// UnmarshalKey mocks base method.
|
||||
func (m *MockInterface) UnmarshalKey(key string, rawVal interface{}, decoderOpts ...viper.DecoderConfigOption) error {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []interface{}{key, rawVal}
|
||||
@@ -196,23 +239,9 @@ func (m *MockInterface) UnmarshalKey(key string, rawVal interface{}, decoderOpts
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UnmarshalKey indicates an expected call of UnmarshalKey
|
||||
// UnmarshalKey indicates an expected call of UnmarshalKey.
|
||||
func (mr *MockInterfaceMockRecorder) UnmarshalKey(key, rawVal interface{}, decoderOpts ...interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]interface{}{key, rawVal}, decoderOpts...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnmarshalKey", reflect.TypeOf((*MockInterface)(nil).UnmarshalKey), varargs...)
|
||||
}
|
||||
|
||||
// GetScanOverrides mocks base method
|
||||
func (m *MockInterface) GetScanOverrides() []models.ScanOverride {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetScanOverrides")
|
||||
ret0, _ := ret[0].([]models.ScanOverride)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetScanOverrides indicates an expected call of GetScanOverrides
|
||||
func (mr *MockInterfaceMockRecorder) GetScanOverrides() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetScanOverrides", reflect.TypeOf((*MockInterface)(nil).GetScanOverrides))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
commands:
|
||||
metrics_scan_args: '--scan --json' # used to detect devices
|
||||
metrics_info_args: '--info --json --device=sat' # used to determine device unique ID & register device with Scrutiny
|
||||
metrics_smart_args: '--xall --json -d sat' # used to retrieve smart data for each device.
|
||||
@@ -0,0 +1,4 @@
|
||||
commands:
|
||||
metrics_scan_args: '--scan' # used to detect devices
|
||||
metrics_info_args: '--info -j' # used to determine device unique ID & register device with Scrutiny
|
||||
metrics_smart_args: '--xall --json' # used to retrieve smart data for each device.
|
||||
@@ -0,0 +1,4 @@
|
||||
commands:
|
||||
metrics_scan_args: '--scan --json' # used to detect devices
|
||||
metrics_info_args: '--info -j' # used to determine device unique ID & register device with Scrutiny
|
||||
metrics_smart_args: '--xall --json -T permissive' # used to retrieve smart data for each device.
|
||||
@@ -0,0 +1,5 @@
|
||||
version: 1
|
||||
devices:
|
||||
- device: /dev/sda
|
||||
commands:
|
||||
metrics_info_args: "--info --json -T permissive"
|
||||
@@ -28,7 +28,8 @@ type Detect struct {
|
||||
// models.Device returned from this function only contain the minimum data for smartctl to execute: device type and device name (device file).
|
||||
func (d *Detect) SmartctlScan() ([]models.Device, error) {
|
||||
//we use smartctl to detect all the drives available.
|
||||
detectedDeviceConnJson, err := d.Shell.Command(d.Logger, "smartctl", []string{"--scan", "-j"}, "", os.Environ())
|
||||
args := strings.Split(d.Config.GetString("commands.metrics_scan_args"), " ")
|
||||
detectedDeviceConnJson, err := d.Shell.Command(d.Logger, "smartctl", args, "", os.Environ())
|
||||
if err != nil {
|
||||
d.Logger.Errorf("Error scanning for devices: %v", err)
|
||||
return nil, err
|
||||
@@ -51,13 +52,13 @@ func (d *Detect) SmartctlScan() ([]models.Device, error) {
|
||||
// - WWN is provided as component data, rather than a "string". We'll have to generate the WWN value ourselves
|
||||
// - WWN from smartctl only provided for ATA protocol drives, NVMe and SCSI drives do not include WWN.
|
||||
func (d *Detect) SmartCtlInfo(device *models.Device) error {
|
||||
|
||||
args := []string{"--info", "-j"}
|
||||
fullDeviceName := fmt.Sprintf("%s%s", DevicePrefix(), device.DeviceName)
|
||||
args := strings.Split(d.Config.GetCommandMetricsInfoArgs(fullDeviceName), " ")
|
||||
//only include the device type if its a non-standard one. In some cases ata drives are detected as scsi in docker, and metadata is lost.
|
||||
if len(device.DeviceType) > 0 && device.DeviceType != "scsi" && device.DeviceType != "ata" {
|
||||
args = append(args, "-d", device.DeviceType)
|
||||
args = append(args, "--device", device.DeviceType)
|
||||
}
|
||||
args = append(args, fmt.Sprintf("%s%s", DevicePrefix(), device.DeviceName))
|
||||
args = append(args, fullDeviceName)
|
||||
|
||||
availableDeviceInfoJson, err := d.Shell.Command(d.Logger, "smartctl", args, "", os.Environ())
|
||||
if err != nil {
|
||||
@@ -138,7 +139,7 @@ func (d *Detect) TransformDetectedDevices(detectedDeviceConns models.Scan) []mod
|
||||
|
||||
//now tha we've "grouped" all the devices, lets override any groups specified in the config file.
|
||||
|
||||
for _, overrideDevice := range d.Config.GetScanOverrides() {
|
||||
for _, overrideDevice := range d.Config.GetDeviceOverrides() {
|
||||
overrideDeviceFile := strings.ToLower(overrideDevice.Device)
|
||||
|
||||
if overrideDevice.Ignore {
|
||||
|
||||
@@ -18,7 +18,8 @@ func TestDetect_SmartctlScan(t *testing.T) {
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||
fakeConfig.EXPECT().GetScanOverrides().AnyTimes().Return([]models.ScanOverride{})
|
||||
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
||||
|
||||
fakeShell := mock_shell.NewMockInterface(mockCtrl)
|
||||
testScanResults, err := ioutil.ReadFile("testdata/smartctl_scan_simple.json")
|
||||
@@ -45,7 +46,8 @@ func TestDetect_SmartctlScan_Megaraid(t *testing.T) {
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||
fakeConfig.EXPECT().GetScanOverrides().AnyTimes().Return([]models.ScanOverride{})
|
||||
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
||||
|
||||
fakeShell := mock_shell.NewMockInterface(mockCtrl)
|
||||
testScanResults, err := ioutil.ReadFile("testdata/smartctl_scan_megaraid.json")
|
||||
@@ -75,7 +77,8 @@ func TestDetect_SmartctlScan_Nvme(t *testing.T) {
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||
fakeConfig.EXPECT().GetScanOverrides().AnyTimes().Return([]models.ScanOverride{})
|
||||
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
||||
|
||||
fakeShell := mock_shell.NewMockInterface(mockCtrl)
|
||||
testScanResults, err := ioutil.ReadFile("testdata/smartctl_scan_nvme.json")
|
||||
@@ -104,7 +107,9 @@ func TestDetect_TransformDetectedDevices_Empty(t *testing.T) {
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||
fakeConfig.EXPECT().GetScanOverrides().AnyTimes().Return([]models.ScanOverride{})
|
||||
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
||||
|
||||
detectedDevices := models.Scan{
|
||||
Devices: []models.ScanDevice{
|
||||
{
|
||||
@@ -134,7 +139,9 @@ func TestDetect_TransformDetectedDevices_Ignore(t *testing.T) {
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||
fakeConfig.EXPECT().GetScanOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda", DeviceType: nil, Ignore: true}})
|
||||
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda", DeviceType: nil, Ignore: true}})
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
||||
|
||||
detectedDevices := models.Scan{
|
||||
Devices: []models.ScanDevice{
|
||||
{
|
||||
@@ -163,7 +170,8 @@ func TestDetect_TransformDetectedDevices_Raid(t *testing.T) {
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||
fakeConfig.EXPECT().GetScanOverrides().AnyTimes().Return([]models.ScanOverride{
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
||||
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{
|
||||
{
|
||||
Device: "/dev/bus/0",
|
||||
DeviceType: []string{"megaraid,14", "megaraid,15", "megaraid,18", "megaraid,19", "megaraid,20", "megaraid,21"},
|
||||
@@ -202,7 +210,8 @@ func TestDetect_TransformDetectedDevices_Simple(t *testing.T) {
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||
fakeConfig.EXPECT().GetScanOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda", DeviceType: []string{"sat+megaraid"}}})
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
||||
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda", DeviceType: []string{"sat+megaraid"}}})
|
||||
detectedDevices := models.Scan{
|
||||
Devices: []models.ScanDevice{
|
||||
{
|
||||
|
||||
@@ -13,7 +13,7 @@ func DevicePrefix() string {
|
||||
|
||||
func (d *Detect) Start() ([]models.Device, error) {
|
||||
d.Shell = shell.Create()
|
||||
// call the base/common functionality to get a list of devicess
|
||||
// call the base/common functionality to get a list of devices
|
||||
detectedDevices, err := d.SmartctlScan()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package detect
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/analogj/scrutiny/collector/pkg/common/shell"
|
||||
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||
"github.com/jaypipes/ghw"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -22,6 +25,7 @@ func (d *Detect) Start() ([]models.Device, error) {
|
||||
//inflate device info for detected devices.
|
||||
for ndx, _ := range detectedDevices {
|
||||
d.SmartCtlInfo(&detectedDevices[ndx]) //ignore errors.
|
||||
populateUdevInfo(&detectedDevices[ndx]) //ignore errors.
|
||||
}
|
||||
|
||||
return detectedDevices, nil
|
||||
@@ -49,3 +53,51 @@ func (d *Detect) wwnFallback(detectedDevice *models.Device) {
|
||||
//wwn must always be lowercase.
|
||||
detectedDevice.WWN = strings.ToLower(detectedDevice.WWN)
|
||||
}
|
||||
|
||||
// as discussed in
|
||||
// - https://github.com/AnalogJ/scrutiny/issues/225
|
||||
// - https://github.com/jaypipes/ghw/issues/59#issue-361915216
|
||||
// udev exposes its data in a standardized way under /run/udev/data/....
|
||||
func populateUdevInfo(detectedDevice *models.Device) error {
|
||||
// Get device major:minor numbers
|
||||
// `cat /sys/class/block/sda/dev`
|
||||
devNo, err := ioutil.ReadFile(filepath.Join("/sys/class/block/", detectedDevice.DeviceName, "dev"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Look up block device in udev runtime database
|
||||
// `cat /run/udev/data/b8:0`
|
||||
udevID := "b" + strings.TrimSpace(string(devNo))
|
||||
udevBytes, err := ioutil.ReadFile(filepath.Join("/run/udev/data/", udevID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deviceMountPaths := []string{}
|
||||
udevInfo := make(map[string]string)
|
||||
for _, udevLine := range strings.Split(string(udevBytes), "\n") {
|
||||
if strings.HasPrefix(udevLine, "E:") {
|
||||
if s := strings.SplitN(udevLine[2:], "=", 2); len(s) == 2 {
|
||||
udevInfo[s[0]] = s[1]
|
||||
}
|
||||
} else if strings.HasPrefix(udevLine, "S:") {
|
||||
deviceMountPaths = append(deviceMountPaths, udevLine[2:])
|
||||
}
|
||||
}
|
||||
|
||||
//Set additional device information.
|
||||
if deviceLabel, exists := udevInfo["ID_FS_LABEL"]; exists {
|
||||
detectedDevice.DeviceLabel = deviceLabel
|
||||
}
|
||||
if deviceUUID, exists := udevInfo["ID_FS_UUID"]; exists {
|
||||
detectedDevice.DeviceUUID = deviceUUID
|
||||
}
|
||||
if deviceSerialID, exists := udevInfo["ID_SERIAL"]; exists {
|
||||
detectedDevice.DeviceSerialID = fmt.Sprintf("%s-%s", udevInfo["ID_BUS"], deviceSerialID)
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package models
|
||||
|
||||
type Device struct {
|
||||
WWN string `json:"wwn"`
|
||||
HostId string `json:"host_id"`
|
||||
WWN string `json:"wwn"`
|
||||
|
||||
DeviceName string `json:"device_name"`
|
||||
DeviceUUID string `json:"device_uuid"`
|
||||
DeviceSerialID string `json:"device_serial_id"`
|
||||
DeviceLabel string `json:"device_label"`
|
||||
|
||||
Manufacturer string `json:"manufacturer"`
|
||||
ModelName string `json:"model_name"`
|
||||
InterfaceType string `json:"interface_type"`
|
||||
@@ -17,6 +20,10 @@ type Device struct {
|
||||
SmartSupport bool `json:"smart_support"`
|
||||
DeviceProtocol string `json:"device_protocol"` //protocol determines which smart attribute types are available (ATA, NVMe, SCSI)
|
||||
DeviceType string `json:"device_type"` //device type is used for querying with -d/t flag, should only be used by collector.
|
||||
|
||||
// User provided metadata
|
||||
Label string `json:"label"`
|
||||
HostId string `json:"host_id"`
|
||||
}
|
||||
|
||||
type DeviceWrapper struct {
|
||||
|
||||
@@ -4,4 +4,8 @@ type ScanOverride struct {
|
||||
Device string `mapstructure:"device"`
|
||||
DeviceType []string `mapstructure:"type"`
|
||||
Ignore bool `mapstructure:"ignore"`
|
||||
Commands struct {
|
||||
MetricsInfoArgs string `mapstructure:"metrics_info_args"`
|
||||
MetricsSmartArgs string `mapstructure:"metrics_smart_args"`
|
||||
} `mapstructure:"commands"`
|
||||
}
|
||||
|
||||
+8
-22
@@ -11,39 +11,25 @@ RUN go mod vendor && \
|
||||
go build -ldflags '-w -extldflags "-static"' -o scrutiny-collector-metrics collector/cmd/collector-metrics/collector-metrics.go
|
||||
|
||||
########
|
||||
FROM node:lts-slim as frontendbuild
|
||||
|
||||
#reduce logging, disable angular-cli analytics for ci environment
|
||||
ENV NPM_CONFIG_LOGLEVEL=warn NG_CLI_ANALYTICS=false
|
||||
|
||||
WORKDIR /opt/scrutiny/src
|
||||
COPY webapp/frontend /opt/scrutiny/src
|
||||
|
||||
RUN npm install -g @angular/cli@9.1.4 && \
|
||||
mkdir -p /scrutiny/dist && \
|
||||
npm install && \
|
||||
ng build --output-path=/opt/scrutiny/dist --prod
|
||||
|
||||
|
||||
########
|
||||
FROM ubuntu:bionic as runtime
|
||||
FROM ubuntu:latest as runtime
|
||||
ARG TARGETARCH
|
||||
EXPOSE 8080
|
||||
WORKDIR /opt/scrutiny
|
||||
ENV PATH="/opt/scrutiny/bin:${PATH}"
|
||||
ENV INFLUXD_CONFIG_PATH=/opt/scrutiny/influxdb
|
||||
|
||||
RUN apt-get update && apt-get install -y cron smartmontools=7.0-0ubuntu1~ubuntu18.04.1 ca-certificates curl tzdata \
|
||||
RUN apt-get update && apt-get install -y cron smartmontools ca-certificates curl tzdata \
|
||||
&& update-ca-certificates \
|
||||
&& case ${TARGETARCH} in \
|
||||
"amd64") S6_ARCH=amd64 ;; \
|
||||
"arm64") S6_ARCH=aarch64 ;; \
|
||||
esac \
|
||||
&& curl https://github.com/just-containers/s6-overlay/releases/download/v1.21.8.0/s6-overlay-${S6_ARCH}.tar.gz -L -s --output /tmp/s6-overlay-${S6_ARCH}.tar.gz \
|
||||
&& tar xzf /tmp/s6-overlay-${S6_ARCH}.tar.gz -C /
|
||||
|
||||
ADD https://dl.influxdata.com/influxdb/releases/influxdb2-2.2.0-${TARGETARCH}.deb /tmp/
|
||||
RUN dpkg -i /tmp/influxdb2-2.2.0-${TARGETARCH}.deb && rm -rf /tmp/influxdb2-2.2.0-${TARGETARCH}.deb
|
||||
&& tar xzf /tmp/s6-overlay-${S6_ARCH}.tar.gz -C / --exclude="./bin" \
|
||||
&& tar xzf /tmp/s6-overlay-${S6_ARCH}.tar.gz -C /usr ./bin \
|
||||
&& rm -rf /tmp/s6-overlay-${S6_ARCH}.tar.gz \
|
||||
&& curl -L https://dl.influxdata.com/influxdb/releases/influxdb2-2.2.0-${TARGETARCH}.deb --output /tmp/influxdb2-2.2.0-${TARGETARCH}.deb \
|
||||
&& dpkg -i --force-all /tmp/influxdb2-2.2.0-${TARGETARCH}.deb
|
||||
|
||||
COPY /rootfs /
|
||||
|
||||
@@ -51,7 +37,7 @@ COPY /rootfs/etc/cron.d/scrutiny /etc/cron.d/scrutiny
|
||||
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny /opt/scrutiny/bin/
|
||||
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny-collector-selftest /opt/scrutiny/bin/
|
||||
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny-collector-metrics /opt/scrutiny/bin/
|
||||
COPY --from=frontendbuild /opt/scrutiny/dist /opt/scrutiny/web
|
||||
COPY dist /opt/scrutiny/web
|
||||
RUN chmod +x /opt/scrutiny/bin/scrutiny && \
|
||||
chmod +x /opt/scrutiny/bin/scrutiny-collector-selftest && \
|
||||
chmod +x /opt/scrutiny/bin/scrutiny-collector-metrics && \
|
||||
|
||||
@@ -10,11 +10,11 @@ RUN go mod vendor && \
|
||||
go build -ldflags '-w -extldflags "-static"' -o scrutiny-collector-metrics collector/cmd/collector-metrics/collector-metrics.go
|
||||
|
||||
########
|
||||
FROM ubuntu:bionic as runtime
|
||||
FROM ubuntu:latest as runtime
|
||||
WORKDIR /scrutiny
|
||||
ENV PATH="/opt/scrutiny/bin:${PATH}"
|
||||
|
||||
RUN apt-get update && apt-get install -y cron smartmontools=7.0-0ubuntu1~ubuntu18.04.1 ca-certificates tzdata && update-ca-certificates
|
||||
RUN apt-get update && apt-get install -y cron smartmontools ca-certificates tzdata && update-ca-certificates
|
||||
|
||||
COPY /docker/entrypoint-collector.sh /entrypoint-collector.sh
|
||||
COPY /rootfs/etc/cron.d/scrutiny /etc/cron.d/scrutiny
|
||||
|
||||
+2
-17
@@ -9,22 +9,7 @@ RUN go mod vendor && \
|
||||
go build -ldflags '-w -extldflags "-static"' -o scrutiny webapp/backend/cmd/scrutiny/scrutiny.go
|
||||
|
||||
########
|
||||
FROM node:lts-slim as frontendbuild
|
||||
|
||||
#reduce logging, disable angular-cli analytics for ci environment
|
||||
ENV NPM_CONFIG_LOGLEVEL=warn NG_CLI_ANALYTICS=false
|
||||
|
||||
WORKDIR /opt/scrutiny/src
|
||||
COPY webapp/frontend /opt/scrutiny/src
|
||||
|
||||
RUN npm install -g @angular/cli@9.1.4 && \
|
||||
mkdir -p /opt/scrutiny/dist && \
|
||||
npm install && \
|
||||
ng build --output-path=/opt/scrutiny/dist --prod
|
||||
|
||||
|
||||
########
|
||||
FROM ubuntu:bionic as runtime
|
||||
FROM ubuntu:latest as runtime
|
||||
EXPOSE 8080
|
||||
WORKDIR /opt/scrutiny
|
||||
ENV PATH="/opt/scrutiny/bin:${PATH}"
|
||||
@@ -32,7 +17,7 @@ ENV PATH="/opt/scrutiny/bin:${PATH}"
|
||||
RUN apt-get update && apt-get install -y ca-certificates curl tzdata && update-ca-certificates
|
||||
|
||||
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny /opt/scrutiny/bin/
|
||||
COPY --from=frontendbuild /opt/scrutiny/dist /opt/scrutiny/web
|
||||
COPY dist /opt/scrutiny/web
|
||||
RUN chmod +x /opt/scrutiny/bin/scrutiny && \
|
||||
mkdir -p /opt/scrutiny/web && \
|
||||
mkdir -p /opt/scrutiny/config && \
|
||||
|
||||
@@ -1 +1 @@
|
||||
> See [docker/example.hubspoke.docker-compose.yml](./docker/example.hubspoke.docker-compose.yml) for a docker-compose file.
|
||||
> See [docker/example.hubspoke.docker-compose.yml](https://github.com/AnalogJ/scrutiny/blob/master/docker/example.hubspoke.docker-compose.yml) for a docker-compose file.
|
||||
|
||||
@@ -57,7 +57,7 @@ web:
|
||||
# and store the information in the config file. If you 're re-using an existing influxdb installation, you'll need to provide
|
||||
# the `token`
|
||||
influxdb:
|
||||
host: 0.0.0.0
|
||||
host: localhost
|
||||
port: 8086
|
||||
# token: 'my-token'
|
||||
# org: 'my-org'
|
||||
@@ -83,9 +83,11 @@ Now that we have downloaded the required files, let's prepare the filesystem.
|
||||
chmod +x /opt/scrutiny/bin/scrutiny-web-linux-amd64
|
||||
|
||||
# Next, lets extract the frontend files.
|
||||
# NOTE: after extraction, there **should not** be a `dist` subdirectory in `/opt/scrutiny/web` directory.
|
||||
cd /opt/scrutiny/web
|
||||
tar xvzf scrutiny-web-frontend.tar.gz --strip-components 1 -C .
|
||||
|
||||
|
||||
# Cleanup
|
||||
rm -rf scrutiny-web-frontend.tar.gz
|
||||
```
|
||||
@@ -113,7 +115,8 @@ Unlike the webapp, the collector does have some dependencies:
|
||||
Unfortunately the version of `smartmontools` (which contains `smartctl`) available in some of the base OS repositories is ancient.
|
||||
So you'll need to install the v7+ version using one of the following commands:
|
||||
|
||||
- **Ubuntu:** `apt-get install -y smartmontools=7.0-0ubuntu1~ubuntu18.04.1`
|
||||
- **Ubuntu (22.04/Jammy/LTS):** `apt-get install -y smartmontools`
|
||||
- **Ubuntu (18.04/Bionic):** `apt-get install -y smartmontools=7.0-0ubuntu1~ubuntu18.04.1`
|
||||
- **Centos8:**
|
||||
- `dnf install https://extras.getpagespeed.com/release-el8-latest.rpm`
|
||||
- `dnf install smartmontools`
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# pfsense Install
|
||||
|
||||
This bascially follows the [Manual collector instructions](https://github.com/AnalogJ/scrutiny/blob/master/docs/INSTALL_MANUAL.md#collector) and assumes you are running a hub and spoke deployment and already have the web app setup.
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
SSH into pfsense, hit `8` for the shell and install the required dependencies.
|
||||
|
||||
```
|
||||
pkg install smartmontools
|
||||
```
|
||||
|
||||
Ensure smartmontools is v7+. This won't be a problem in pfsense 2.6.0+
|
||||
|
||||
|
||||
### Directory Structure
|
||||
|
||||
Now let's create a directory structure to contain the Scrutiny collector binary.
|
||||
|
||||
```
|
||||
mkdir -p /opt/scrutiny/bin
|
||||
```
|
||||
|
||||
|
||||
### Download Files
|
||||
|
||||
Next, we'll download the Scrutiny collector binary from the [latest Github release](https://github.com/analogj/scrutiny/releases).
|
||||
|
||||
> NOTE: Ensure you have the latest version in the below command
|
||||
|
||||
```
|
||||
fetch -o /opt/scrutiny/bin https://github.com/AnalogJ/scrutiny/releases/download/vX.X.X/scrutiny-collector-metrics-freebsd-amd64
|
||||
```
|
||||
|
||||
|
||||
### Prepare Scrutiny
|
||||
|
||||
Now that we have downloaded the required files, let's prepare the filesystem.
|
||||
|
||||
```
|
||||
chmod +x /opt/scrutiny/bin/scrutiny-collector-metrics-freebsd-amd64
|
||||
```
|
||||
|
||||
|
||||
### Start Scrutiny Collector, Populate Webapp
|
||||
|
||||
Next, we will manually trigger the collector, to populate the Scrutiny dashboard:
|
||||
|
||||
> NOTE: if you need to pass a config file to the scrutiny collector, you can provide it using the `--config` flag.
|
||||
|
||||
```
|
||||
/opt/scrutiny/bin/scrutiny-collector-metrics-freebsd-amd64 run --api-endpoint "http://localhost:8080"
|
||||
```
|
||||
> NOTE: change the IP address to that of your web app
|
||||
|
||||
### Schedule Collector with Cron
|
||||
|
||||
Finally you need to schedule the collector to run periodically.
|
||||
|
||||
Login to the pfsense webGUI and head to `Services/Cron` add an entry with the following details:
|
||||
|
||||
```
|
||||
Minute: */15
|
||||
Hour: *
|
||||
Day of the Month: *
|
||||
Month of the Year: *
|
||||
Day of the Week: *
|
||||
User: root
|
||||
Command: /opt/scrutiny/bin/scrutiny-collector-metrics-freebsd-amd64 run --api-endpoint "http://localhost:8080" >/dev/null 2>&1
|
||||
```
|
||||
> NOTE: `>/dev/null 2>&1` is used to stop cron confirmation emails being sent.
|
||||
@@ -4,11 +4,13 @@ These are the officially supported NAS OS's (with documentation and setup guides
|
||||
Once a guide is created (in `docs/guides/`) it will be linked here.
|
||||
|
||||
- [ ] freenas/truenas
|
||||
- [x] [unraid](https://github.com/AnalogJ/scrutiny/blob/master/docs/INSTALL_UNRAID.md)
|
||||
- [x] [unraid](./INSTALL_UNRAID.md)
|
||||
- [ ] ESXI
|
||||
- [ ] Proxmox
|
||||
- [ ] Synology
|
||||
- [ ] OMV
|
||||
- [ ] Amahi
|
||||
- [ ] Running in a LXC container
|
||||
- [x] [PFSense](./INSTALL_UNRAID.md)
|
||||
- [ ] QNAP
|
||||
|
||||
|
||||
@@ -52,6 +52,8 @@ If the output is the same, your devices will be processed by Scrutiny.
|
||||
In some cases `--scan` does not correctly detect the device type, returning [incomplete SMART data](https://github.com/AnalogJ/scrutiny/issues/45).
|
||||
Scrutiny will supports overriding the detected device type via the config file.
|
||||
|
||||
[example.collector.yaml](https://github.com/AnalogJ/scrutiny/blob/master/example.collector.yaml)
|
||||
|
||||
### RAID Controllers (Megaraid/3ware/HBA/Adaptec/HPE/etc)
|
||||
Smartctl has support for a large number of [RAID controllers](https://www.smartmontools.org/wiki/Supported_RAID-Controllers), however this
|
||||
support is not automatic, and may require some additional device type hinting. You can provide this information to the Scrutiny collector
|
||||
@@ -111,12 +113,60 @@ instead of the block device (`/dev/nvme0n1`). See [#209](https://github.com/Anal
|
||||
|
||||
### ATA
|
||||
|
||||
### Standby/Sleeping Disks
|
||||
### Exit Codes
|
||||
|
||||
If you see an error message similar to `smartctl returned an error code (2) while processing /dev/sda`, this means that
|
||||
`smartctl` (not Scrutiny) exited with an error code. Scrutiny will attempt to print a helpful error message to help you debug,
|
||||
but you can look at the table (and associated links) below to debug `smartctl`.
|
||||
|
||||
> smartctl Return Values
|
||||
> The return values of smartctl are defined by a bitmask. If all is well with the disk, the return value (exit status) of
|
||||
> smartctl is 0 (all bits turned off). If a problem occurs, or an error, potential error, or fault is detected, then
|
||||
> a non-zero status is returned. In this case, the eight different bits in the return value have the following meanings
|
||||
> for ATA disks; some of these values may also be returned for SCSI disks.
|
||||
>
|
||||
> source: http://www.linuxguide.it/command_line/linux-manpage/do.php?file=smartctl#sect7
|
||||
|
||||
|
||||
| Exit Code (Isolated) | Binary | Problem Message |
|
||||
| --- | --- | --- |
|
||||
| 1 | Bit 0 | Command line did not parse. |
|
||||
| 2 | Bit 1 | Device open failed, or device did not return an IDENTIFY DEVICE structure. |
|
||||
| 4 | Bit 2 | Some SMART command to the disk failed, or there was a checksum error in a SMART data structure (see В´-bВ´ option above). |
|
||||
| 8 | Bit 3 | SMART status check returned “DISK FAILING". |
|
||||
| 16 | Bit 4 | We found prefail Attributes <= threshold. |
|
||||
| 32 | Bit 5 | SMART status check returned “DISK OK” but we found that some (usage or prefail) Attributes have been <= threshold at some time in the past. |
|
||||
| 64 | Bit 6 | The device error log contains records of errors. |
|
||||
| 128 | Bit 7 | The device self-test log contains records of errors. |
|
||||
|
||||
#### Standby/Sleeping Disks
|
||||
|
||||
Disks in Standby/Sleep can also cause `smartctl` to exit abnormally, usually with `exit code: 2`.
|
||||
|
||||
- https://github.com/AnalogJ/scrutiny/issues/221
|
||||
- https://github.com/AnalogJ/scrutiny/issues/157
|
||||
|
||||
### Volume Mount All Devices (`/dev`) - Privileged
|
||||
|
||||
> WARNING: This is an insecure/dangerous workaround. Running Scrutiny (or any Docker image) with `--privileged` is equivalent to running it with root access.
|
||||
|
||||
If you have exhausted all other mechanisms to get your disks working with `smartctl` running within a container, you can try running the docker image with the following additional flags:
|
||||
|
||||
- `--privileged` (instead of `--cap-add`) - this gives the docker container full access to your system. Scrutiny does not require this permission, however it can be helpful for `smartctl`
|
||||
- `-v /dev:/dev:ro` (instead of `--device`) - this mounts the `/dev` folder (containing all your device files) into the container, allowing `smartctl` to see your disks, exactly as if it were running on your host directly.
|
||||
|
||||
With this workaround your `docker run` command would look similar to the following:
|
||||
|
||||
```bash
|
||||
docker run -it --rm -p 8080:8080 -p 8086:8086 \
|
||||
-v `pwd`/scrutiny:/opt/scrutiny/config \
|
||||
-v `pwd`/influxdb2:/opt/scrutiny/influxdb \
|
||||
-v /run/udev:/run/udev:ro \
|
||||
--privileged \
|
||||
-v /dev:/dev \
|
||||
--name scrutiny \
|
||||
ghcr.io/analogj/scrutiny:master-omnibus
|
||||
```
|
||||
|
||||
## Scrutiny detects Failure but SMART Passed?
|
||||
|
||||
@@ -138,3 +188,17 @@ Thankfully the collector has a special `--host-id` flag (or `COLLECTOR_HOST_ID`
|
||||
|
||||
See the [docs/INSTALL_HUB_SPOKE.md](/docs/INSTALL_HUB_SPOKE.md) guide for more information.
|
||||
|
||||
## Collector DEBUG mode
|
||||
|
||||
You can use environmental variables to enable debug logging and/or log files for the collector:
|
||||
|
||||
```bash
|
||||
DEBUG=true
|
||||
COLLECTOR_LOG_FILE=/tmp/collector.log
|
||||
```
|
||||
|
||||
Or if you're not using docker, you can pass CLI arguments to the collector during startup:
|
||||
|
||||
```bash
|
||||
scrutiny-collector-metrics run --debug --log-file /tmp/collector.log
|
||||
```
|
||||
@@ -0,0 +1,25 @@
|
||||
# Notifications
|
||||
|
||||
As documented in [example.scrutiny.yaml](https://github.com/AnalogJ/scrutiny/blob/master/example.scrutiny.yaml#L59-L75)
|
||||
there are multiple ways to configure notifications for Scrutiny.
|
||||
|
||||
Under the hood we use a library called [Shoutrrr](https://github.com/containrrr/shoutrrr) to send our notifications, and you should use their documentation if you run into
|
||||
any issues: https://containrrr.dev/shoutrrr/services/overview/
|
||||
|
||||
|
||||
# Script Notifications
|
||||
|
||||
While the Shoutrrr library supports many popular providers for sending notifications Scrutiny also supports a "script" based
|
||||
notification system, allowing you to execute a custom script whenever a notification needs to be sent.
|
||||
Data is provided to this script using the following environmental variables:
|
||||
|
||||
```
|
||||
SCRUTINY_SUBJECT - eg. "Scrutiny SMART error (%s) detected on device: %s"
|
||||
SCRUTINY_DATE
|
||||
SCRUTINY_FAILURE_TYPE - EmailTest, SmartFail, ScrutinyFail
|
||||
SCRUTINY_DEVICE_NAME - eg. /dev/sda
|
||||
SCRUTINY_DEVICE_TYPE - ATA/SCSI/NVMe
|
||||
SCRUTINY_DEVICE_SERIAL - eg. WDDJ324KSO
|
||||
SCRUTINY_MESSAGE - eg. "Scrutiny SMART error notification for device: %s\nFailure Type: %s\nDevice Name: %s\nDevice Serial: %s\nDevice Type: %s\nDate: %s"
|
||||
```
|
||||
|
||||
@@ -55,4 +55,52 @@ api:
|
||||
You may also configure these values using the following environmental variables (both are required).
|
||||
|
||||
- `COLLECTOR_API_ENDPOINT=http://localhost:8080/custombasepath`
|
||||
- `SCRUTINY_WEB_LISTEN_BASEPATH=/custombasepath`
|
||||
- `SCRUTINY_WEB_LISTEN_BASEPATH=/custombasepath`
|
||||
|
||||
# Real Examples
|
||||
|
||||
## Caddy
|
||||
|
||||
1. Create a Caddyfile
|
||||
```yaml
|
||||
# Caddyfile
|
||||
:9090
|
||||
|
||||
# The `scrutiny` text in this file must match the service name in the docker-compose file below.
|
||||
# The `/custom/` text is the custom base path scrutiny will be availble on.
|
||||
reverse_proxy /custom/* scrutiny:8080
|
||||
|
||||
```
|
||||
2. Create a `docker-compose.yml` file
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
version: '3.5'
|
||||
|
||||
services:
|
||||
scrutiny:
|
||||
container_name: scrutiny
|
||||
image: ghcr.io/analogj/scrutiny:master-omnibus
|
||||
cap_add:
|
||||
- SYS_RAWIO
|
||||
ports:
|
||||
- "8086:8086" # influxDB admin
|
||||
volumes:
|
||||
- /run/udev:/run/udev:ro
|
||||
- ./config:/opt/scrutiny/config
|
||||
- ./influxdb:/opt/scrutiny/influxdb
|
||||
devices:
|
||||
- "/dev/sda"
|
||||
- "/dev/sdb"
|
||||
environment:
|
||||
- SCRUTINY_WEB_LISTEN_BASEPATH=/custom
|
||||
- COLLECTOR_API_ENDPOINT=http://localhost:8080/custom
|
||||
caddy:
|
||||
image: caddy
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile
|
||||
ports:
|
||||
- "9090:9090"
|
||||
```
|
||||
3. run `docker-compose up`
|
||||
4. visit [http://localhost:9090/custom/web](http://localhost:9090/custom/web) - access the scrutiny container via caddy reverse proxy
|
||||
@@ -53,6 +53,13 @@ devices:
|
||||
# - 3ware,3
|
||||
# - 3ware,4
|
||||
# - 3ware,5
|
||||
#
|
||||
# # example to show how to override the smartctl command args (per device), see below for how to override these globally.
|
||||
# - device: /dev/sda
|
||||
# commands:
|
||||
# metrics_info_args: '--info --json -T permissive' # used to determine device unique ID & register device with Scrutiny
|
||||
# metrics_smart_args: '--xall --json -T permissive' # used to retrieve smart data for each device.
|
||||
|
||||
|
||||
#log:
|
||||
# file: '' #absolute or relative paths allowed, eg. web.log
|
||||
@@ -64,6 +71,12 @@ devices:
|
||||
# if you need to use a custom base path (for a reverse proxy), you can add a suffix to the endpoint.
|
||||
# See docs/TROUBLESHOOTING_REVERSE_PROXY.md for more info,
|
||||
|
||||
# example to show how to override the smartctl command args globally
|
||||
#commands:
|
||||
# metrics_scan_args: '--scan --json' # used to detect devices
|
||||
# metrics_info_args: '--info --json' # used to determine device unique ID & register device with Scrutiny
|
||||
# metrics_smart_args: '--xall --json' # used to retrieve smart data for each device.
|
||||
|
||||
|
||||
########################################################################################################################
|
||||
# FEATURES COMING SOON
|
||||
|
||||
@@ -40,6 +40,7 @@ web:
|
||||
# and store the information in the config file. If you 're re-using an existing influxdb installation, you'll need to provide
|
||||
# the `token`
|
||||
influxdb:
|
||||
# scheme: 'http'
|
||||
host: 0.0.0.0
|
||||
port: 8086
|
||||
# token: 'my-token'
|
||||
|
||||
@@ -39,6 +39,7 @@ func (c *configuration) Init() error {
|
||||
|
||||
c.SetDefault("notify.urls", []string{})
|
||||
|
||||
c.SetDefault("web.influxdb.scheme", "http")
|
||||
c.SetDefault("web.influxdb.host", "localhost")
|
||||
c.SetDefault("web.influxdb.port", "8086")
|
||||
c.SetDefault("web.influxdb.org", "scrutiny")
|
||||
|
||||
@@ -4,25 +4,34 @@ const DeviceProtocolAta = "ATA"
|
||||
const DeviceProtocolScsi = "SCSI"
|
||||
const DeviceProtocolNvme = "NVMe"
|
||||
|
||||
const SmartAttributeStatusPassed = 0
|
||||
const SmartAttributeStatusFailed = 1
|
||||
const SmartAttributeStatusWarning = 2
|
||||
|
||||
const SmartWhenFailedFailingNow = "FAILING_NOW"
|
||||
const SmartWhenFailedInThePast = "IN_THE_PAST"
|
||||
|
||||
//const SmartStatusPassed = "passed"
|
||||
//const SmartStatusFailed = "failed"
|
||||
|
||||
type DeviceStatus int
|
||||
type AttributeStatus uint8
|
||||
|
||||
const (
|
||||
DeviceStatusPassed DeviceStatus = 0
|
||||
DeviceStatusFailedSmart DeviceStatus = iota
|
||||
DeviceStatusFailedScrutiny DeviceStatus = iota
|
||||
// AttributeStatusPassed binary, 1,2,4,8,16,32,etc
|
||||
AttributeStatusPassed AttributeStatus = 0
|
||||
AttributeStatusFailedSmart AttributeStatus = 1
|
||||
AttributeStatusWarningScrutiny AttributeStatus = 2
|
||||
AttributeStatusFailedScrutiny AttributeStatus = 4
|
||||
)
|
||||
|
||||
func Set(b, flag DeviceStatus) DeviceStatus { return b | flag }
|
||||
func Clear(b, flag DeviceStatus) DeviceStatus { return b &^ flag }
|
||||
func Toggle(b, flag DeviceStatus) DeviceStatus { return b ^ flag }
|
||||
func Has(b, flag DeviceStatus) bool { return b&flag != 0 }
|
||||
const AttributeWhenFailedFailingNow = "FAILING_NOW"
|
||||
const AttributeWhenFailedInThePast = "IN_THE_PAST"
|
||||
|
||||
func AttributeStatusSet(b, flag AttributeStatus) AttributeStatus { return b | flag }
|
||||
func AttributeStatusClear(b, flag AttributeStatus) AttributeStatus { return b &^ flag }
|
||||
func AttributeStatusToggle(b, flag AttributeStatus) AttributeStatus { return b ^ flag }
|
||||
func AttributeStatusHas(b, flag AttributeStatus) bool { return b&flag != 0 }
|
||||
|
||||
type DeviceStatus uint8
|
||||
|
||||
const (
|
||||
// DeviceStatusPassed binary, 1,2,4,8,16,32,etc
|
||||
DeviceStatusPassed DeviceStatus = 0
|
||||
DeviceStatusFailedSmart DeviceStatus = 1
|
||||
DeviceStatusFailedScrutiny DeviceStatus = 2
|
||||
)
|
||||
|
||||
func DeviceStatusSet(b, flag DeviceStatus) DeviceStatus { return b | flag }
|
||||
func DeviceStatusClear(b, flag DeviceStatus) DeviceStatus { return b &^ flag }
|
||||
func DeviceStatusToggle(b, flag DeviceStatus) DeviceStatus { return b ^ flag }
|
||||
func DeviceStatusHas(b, flag DeviceStatus) bool { return b&flag != 0 }
|
||||
|
||||
@@ -19,6 +19,7 @@ type DeviceRepo interface {
|
||||
UpdateDevice(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (models.Device, error)
|
||||
UpdateDeviceStatus(ctx context.Context, wwn string, status pkg.DeviceStatus) (models.Device, error)
|
||||
GetDeviceDetails(ctx context.Context, wwn string) (models.Device, error)
|
||||
DeleteDevice(ctx context.Context, wwn string) error
|
||||
|
||||
SaveSmartAttributes(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (measurements.Smart, error)
|
||||
GetSmartAttributeHistory(ctx context.Context, wwn string, durationKey string, attributes []string) ([]measurements.Smart, error)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Deprecated: m20220503120000.Device is deprecated, only used by db migrations
|
||||
type Device struct {
|
||||
//GORM attributes, see: http://gorm.io/docs/conventions.html
|
||||
CreatedAt time.Time
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package m20220509170100
|
||||
|
||||
import (
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Device struct {
|
||||
//GORM attributes, see: http://gorm.io/docs/conventions.html
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
|
||||
WWN string `json:"wwn" gorm:"primary_key"`
|
||||
|
||||
DeviceName string `json:"device_name"`
|
||||
DeviceUUID string `json:"device_uuid"`
|
||||
DeviceSerialID string `json:"device_serial_id"`
|
||||
DeviceLabel string `json:"device_label"`
|
||||
|
||||
Manufacturer string `json:"manufacturer"`
|
||||
ModelName string `json:"model_name"`
|
||||
InterfaceType string `json:"interface_type"`
|
||||
InterfaceSpeed string `json:"interface_speed"`
|
||||
SerialNumber string `json:"serial_number"`
|
||||
Firmware string `json:"firmware"`
|
||||
RotationSpeed int `json:"rotational_speed"`
|
||||
Capacity int64 `json:"capacity"`
|
||||
FormFactor string `json:"form_factor"`
|
||||
SmartSupport bool `json:"smart_support"`
|
||||
DeviceProtocol string `json:"device_protocol"` //protocol determines which smart attribute types are available (ATA, NVMe, SCSI)
|
||||
DeviceType string `json:"device_type"` //device type is used for querying with -d/t flag, should only be used by collector.
|
||||
|
||||
// User provided metadata
|
||||
Label string `json:"label"`
|
||||
HostId string `json:"host_id"`
|
||||
|
||||
// Data set by Scrutiny
|
||||
DeviceStatus pkg.DeviceStatus `json:"device_status"`
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ func NewScrutinyRepository(appConfig config.Interface, globalLogger logrus.Field
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Create a new client using an InfluxDB server base URL and an authentication token
|
||||
influxdbUrl := fmt.Sprintf("http://%s:%s", appConfig.GetString("web.influxdb.host"), appConfig.GetString("web.influxdb.port"))
|
||||
influxdbUrl := fmt.Sprintf("%s://%s:%s", appConfig.GetString("web.influxdb.scheme"), appConfig.GetString("web.influxdb.host"), appConfig.GetString("web.influxdb.port"))
|
||||
globalLogger.Debugf("InfluxDB url: %s", influxdbUrl)
|
||||
|
||||
client := influxdb2.NewClient(influxdbUrl, appConfig.GetString("web.influxdb.token"))
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||
"gorm.io/gorm/clause"
|
||||
"time"
|
||||
)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -18,7 +19,7 @@ import (
|
||||
func (sr *scrutinyRepository) RegisterDevice(ctx context.Context, dev models.Device) error {
|
||||
if err := sr.gormClient.WithContext(ctx).Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "wwn"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"host_id", "device_name", "device_type"}),
|
||||
DoUpdates: clause.AssignmentColumns([]string{"host_id", "device_name", "device_type", "device_uuid", "device_serial_id", "device_label"}),
|
||||
}).Create(&dev).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -57,7 +58,7 @@ func (sr *scrutinyRepository) UpdateDeviceStatus(ctx context.Context, wwn string
|
||||
return device, fmt.Errorf("Could not get device from DB: %v", err)
|
||||
}
|
||||
|
||||
device.DeviceStatus = pkg.Set(device.DeviceStatus, status)
|
||||
device.DeviceStatus = pkg.DeviceStatusSet(device.DeviceStatus, status)
|
||||
return device, sr.gormClient.Model(&device).Updates(device).Error
|
||||
}
|
||||
|
||||
@@ -72,3 +73,33 @@ func (sr *scrutinyRepository) GetDeviceDetails(ctx context.Context, wwn string)
|
||||
|
||||
return device, nil
|
||||
}
|
||||
|
||||
func (sr *scrutinyRepository) DeleteDevice(ctx context.Context, wwn string) error {
|
||||
if err := sr.gormClient.WithContext(ctx).Where("wwn = ?", wwn).Delete(&models.Device{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//delete data from influxdb.
|
||||
buckets := []string{
|
||||
sr.appConfig.GetString("web.influxdb.bucket"),
|
||||
fmt.Sprintf("%s_weekly", sr.appConfig.GetString("web.influxdb.bucket")),
|
||||
fmt.Sprintf("%s_monthly", sr.appConfig.GetString("web.influxdb.bucket")),
|
||||
fmt.Sprintf("%s_yearly", sr.appConfig.GetString("web.influxdb.bucket")),
|
||||
}
|
||||
|
||||
for _, bucket := range buckets {
|
||||
sr.logger.Infof("Deleting data for %s in bucket: %s", wwn, bucket)
|
||||
if err := sr.influxClient.DeleteAPI().DeleteWithName(
|
||||
ctx,
|
||||
sr.appConfig.GetString("web.influxdb.org"),
|
||||
bucket,
|
||||
time.Now().AddDate(-10, 0, 0),
|
||||
time.Now(),
|
||||
fmt.Sprintf(`device_wwn="%s"`, wwn),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,14 +2,18 @@ package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20201107210306"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220503120000"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220509170100"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"github.com/influxdata/influxdb-client-go/v2/api/http"
|
||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -22,9 +26,12 @@ import (
|
||||
|
||||
func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
|
||||
sr.logger.Infoln("Database migration starting")
|
||||
sr.logger.Infoln("Database migration starting. Please wait, this process may take a long time....")
|
||||
|
||||
m := gormigrate.New(sr.gormClient, gormigrate.DefaultOptions, []*gormigrate.Migration{
|
||||
gormMigrateOptions := gormigrate.DefaultOptions
|
||||
gormMigrateOptions.UseTransaction = true
|
||||
|
||||
m := gormigrate.New(sr.gormClient, gormMigrateOptions, []*gormigrate.Migration{
|
||||
{
|
||||
ID: "20201107210306", // v0.3.13 (pre-influxdb schema). 9fac3c6308dc6cb6cd5bbc43a68cd93e8fb20b87
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
@@ -38,16 +45,6 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
&m20201107210306.SmartNvmeAttribute{},
|
||||
)
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.Migrator().DropTable(
|
||||
&m20201107210306.Device{},
|
||||
&m20201107210306.Smart{},
|
||||
&m20201107210306.SmartAtaAttribute{},
|
||||
&m20201107210306.SmartNvmeAttribute{},
|
||||
&m20201107210306.SmartNvmeAttribute{},
|
||||
"self_tests",
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "20220503113100", // backwards compatible - influxdb schema
|
||||
@@ -137,7 +134,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
smartTags,
|
||||
smartFields,
|
||||
postSmartResults.Date, ctx)
|
||||
if err != nil {
|
||||
if ignorePastRetentionPolicyError(err) != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -147,7 +144,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
tempTags,
|
||||
tempFields,
|
||||
postSmartResults.Date, ctx)
|
||||
if err != nil {
|
||||
if ignorePastRetentionPolicyError(err) != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -166,7 +163,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
smartFields,
|
||||
postSmartResults.Date, ctx)
|
||||
|
||||
if err != nil {
|
||||
if ignorePastRetentionPolicyError(err) != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -176,7 +173,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
tempTags,
|
||||
tempFields,
|
||||
postSmartResults.Date, ctx)
|
||||
if err != nil {
|
||||
if ignorePastRetentionPolicyError(err) != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -194,7 +191,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
smartTags,
|
||||
smartFields,
|
||||
postSmartResults.Date, ctx)
|
||||
if err != nil {
|
||||
if ignorePastRetentionPolicyError(err) != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -204,7 +201,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
tempTags,
|
||||
tempFields,
|
||||
postSmartResults.Date, ctx)
|
||||
if err != nil {
|
||||
if ignorePastRetentionPolicyError(err) != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -221,7 +218,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
smartTags,
|
||||
smartFields,
|
||||
postSmartResults.Date, ctx)
|
||||
if err != nil {
|
||||
if ignorePastRetentionPolicyError(err) != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -231,7 +228,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
tempTags,
|
||||
tempFields,
|
||||
postSmartResults.Date, ctx)
|
||||
if err != nil {
|
||||
if ignorePastRetentionPolicyError(err) != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -257,20 +254,44 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
//migrate the device database to the current version
|
||||
//migrate the device database
|
||||
return tx.AutoMigrate(m20220503120000.Device{})
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "m20220509170100", // addl udev device data
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
|
||||
//migrate the device database.
|
||||
// adding addl columns (device_label, device_uuid, device_serial_id)
|
||||
return tx.AutoMigrate(m20220509170100.Device{})
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if err := m.Migrate(); err != nil {
|
||||
sr.logger.Errorf("Database migration failed with error: %w", err)
|
||||
sr.logger.Errorf("Database migration failed with error. \n Please open a github issue at https://github.com/AnalogJ/scrutiny and attach a copy of your scrutiny.db file. \n %v", err)
|
||||
return err
|
||||
}
|
||||
sr.logger.Infoln("Database migration completed successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// helpers
|
||||
|
||||
//When adding data to influxdb, an error may be returned if the data point is outside the range of the retention policy.
|
||||
//This function will ignore retention policy errors, and allow the migration to continue.
|
||||
func ignorePastRetentionPolicyError(err error) error {
|
||||
var influxDbWriteError *http.Error
|
||||
if errors.As(err, &influxDbWriteError) {
|
||||
if influxDbWriteError.StatusCode == 422 {
|
||||
log.Infoln("ignoring error: attempted to writePoint past retention period duration")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Deprecated
|
||||
func m20201107210306_FromPreInfluxDBTempCreatePostInfluxDBTemp(preDevice m20201107210306.Device, preSmartResult m20201107210306.Smart) (error, measurements.SmartTemperature) {
|
||||
//extract temperature data for every datapoint
|
||||
|
||||
@@ -21,6 +21,10 @@ type Device struct {
|
||||
WWN string `json:"wwn" gorm:"primary_key"`
|
||||
|
||||
DeviceName string `json:"device_name"`
|
||||
DeviceUUID string `json:"device_uuid"`
|
||||
DeviceSerialID string `json:"device_serial_id"`
|
||||
DeviceLabel string `json:"device_label"`
|
||||
|
||||
Manufacturer string `json:"manufacturer"`
|
||||
ModelName string `json:"model_name"`
|
||||
InterfaceType string `json:"interface_type"`
|
||||
@@ -162,7 +166,7 @@ func (dv *Device) UpdateFromCollectorSmartInfo(info collector.SmartInfo) error {
|
||||
dv.DeviceProtocol = info.Device.Protocol
|
||||
|
||||
if !info.SmartStatus.Passed {
|
||||
dv.DeviceStatus = pkg.Set(dv.DeviceStatus, pkg.DeviceStatusFailedSmart)
|
||||
dv.DeviceStatus = pkg.DeviceStatusSet(dv.DeviceStatus, pkg.DeviceStatusFailedSmart)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -110,7 +110,7 @@ func (sm *Smart) FromCollectorSmartInfo(wwn string, info collector.SmartInfo) er
|
||||
sm.PowerCycleCount = info.PowerCycleCount
|
||||
sm.PowerOnHours = info.PowerOnTime.Hours
|
||||
if !info.SmartStatus.Passed {
|
||||
sm.Status = pkg.DeviceStatusFailedSmart
|
||||
sm.Status = pkg.DeviceStatusSet(sm.Status, pkg.DeviceStatusFailedSmart)
|
||||
}
|
||||
|
||||
sm.DeviceProtocol = info.Device.Protocol
|
||||
@@ -148,8 +148,9 @@ func (sm *Smart) ProcessAtaSmartInfo(tableItems []collector.AtaSmartAttributesTa
|
||||
}
|
||||
attrModel.PopulateAttributeStatus()
|
||||
sm.Attributes[strconv.Itoa(collectorAttr.ID)] = &attrModel
|
||||
if attrModel.Status == pkg.SmartAttributeStatusFailed {
|
||||
sm.Status = pkg.Set(sm.Status, pkg.DeviceStatusFailedScrutiny)
|
||||
|
||||
if pkg.AttributeStatusHas(attrModel.Status, pkg.AttributeStatusFailedScrutiny) {
|
||||
sm.Status = pkg.DeviceStatusSet(sm.Status, pkg.DeviceStatusFailedScrutiny)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -171,15 +172,15 @@ func (sm *Smart) ProcessNvmeSmartInfo(nvmeSmartHealthInformationLog collector.Nv
|
||||
"power_on_hours": (&SmartNvmeAttribute{AttributeId: "power_on_hours", Value: nvmeSmartHealthInformationLog.PowerOnHours, Threshold: -1}).PopulateAttributeStatus(),
|
||||
"unsafe_shutdowns": (&SmartNvmeAttribute{AttributeId: "unsafe_shutdowns", Value: nvmeSmartHealthInformationLog.UnsafeShutdowns, Threshold: -1}).PopulateAttributeStatus(),
|
||||
"media_errors": (&SmartNvmeAttribute{AttributeId: "media_errors", Value: nvmeSmartHealthInformationLog.MediaErrors, Threshold: 0}).PopulateAttributeStatus(),
|
||||
"num_err_log_entries": (&SmartNvmeAttribute{AttributeId: "num_err_log_entries", Value: nvmeSmartHealthInformationLog.NumErrLogEntries, Threshold: 0}).PopulateAttributeStatus(),
|
||||
"num_err_log_entries": (&SmartNvmeAttribute{AttributeId: "num_err_log_entries", Value: nvmeSmartHealthInformationLog.NumErrLogEntries, Threshold: -1}).PopulateAttributeStatus(),
|
||||
"warning_temp_time": (&SmartNvmeAttribute{AttributeId: "warning_temp_time", Value: nvmeSmartHealthInformationLog.WarningTempTime, Threshold: -1}).PopulateAttributeStatus(),
|
||||
"critical_comp_time": (&SmartNvmeAttribute{AttributeId: "critical_comp_time", Value: nvmeSmartHealthInformationLog.CriticalCompTime, Threshold: -1}).PopulateAttributeStatus(),
|
||||
}
|
||||
|
||||
//find analyzed attribute status
|
||||
for _, val := range sm.Attributes {
|
||||
if val.GetStatus() == pkg.SmartAttributeStatusFailed {
|
||||
sm.Status = pkg.Set(sm.Status, pkg.DeviceStatusFailedScrutiny)
|
||||
if pkg.AttributeStatusHas(val.GetStatus(), pkg.AttributeStatusFailedScrutiny) {
|
||||
sm.Status = pkg.DeviceStatusSet(sm.Status, pkg.DeviceStatusFailedScrutiny)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,8 +205,8 @@ func (sm *Smart) ProcessScsiSmartInfo(defectGrownList int64, scsiErrorCounterLog
|
||||
|
||||
//find analyzed attribute status
|
||||
for _, val := range sm.Attributes {
|
||||
if val.GetStatus() == pkg.SmartAttributeStatusFailed {
|
||||
sm.Status = pkg.Set(sm.Status, pkg.DeviceStatusFailedScrutiny)
|
||||
if pkg.AttributeStatusHas(val.GetStatus(), pkg.AttributeStatusFailedScrutiny) {
|
||||
sm.Status = pkg.DeviceStatusSet(sm.Status, pkg.DeviceStatusFailedScrutiny)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,13 +18,13 @@ type SmartAtaAttribute struct {
|
||||
WhenFailed string `json:"when_failed"`
|
||||
|
||||
//Generated data
|
||||
TransformedValue int64 `json:"transformed_value"`
|
||||
Status int64 `json:"status"`
|
||||
StatusReason string `json:"status_reason,omitempty"`
|
||||
FailureRate float64 `json:"failure_rate,omitempty"`
|
||||
TransformedValue int64 `json:"transformed_value"`
|
||||
Status pkg.AttributeStatus `json:"status"`
|
||||
StatusReason string `json:"status_reason,omitempty"`
|
||||
FailureRate float64 `json:"failure_rate,omitempty"`
|
||||
}
|
||||
|
||||
func (sa *SmartAtaAttribute) GetStatus() int64 {
|
||||
func (sa *SmartAtaAttribute) GetStatus() pkg.AttributeStatus {
|
||||
return sa.Status
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ func (sa *SmartAtaAttribute) Flatten() map[string]interface{} {
|
||||
|
||||
//Generated Data
|
||||
fmt.Sprintf("attr.%s.transformed_value", idString): sa.TransformedValue,
|
||||
fmt.Sprintf("attr.%s.status", idString): sa.Status,
|
||||
fmt.Sprintf("attr.%s.status", idString): int64(sa.Status),
|
||||
fmt.Sprintf("attr.%s.status_reason", idString): sa.StatusReason,
|
||||
fmt.Sprintf("attr.%s.failure_rate", idString): sa.FailureRate,
|
||||
}
|
||||
@@ -77,7 +77,7 @@ func (sa *SmartAtaAttribute) Inflate(key string, val interface{}) {
|
||||
case "transformed_value":
|
||||
sa.TransformedValue = val.(int64)
|
||||
case "status":
|
||||
sa.Status = val.(int64)
|
||||
sa.Status = pkg.AttributeStatus(val.(int64))
|
||||
case "status_reason":
|
||||
sa.StatusReason = val.(string)
|
||||
case "failure_rate":
|
||||
@@ -89,16 +89,16 @@ func (sa *SmartAtaAttribute) Inflate(key string, val interface{}) {
|
||||
//populate attribute status, using SMART Thresholds & Observed Metadata
|
||||
// Chainable
|
||||
func (sa *SmartAtaAttribute) PopulateAttributeStatus() *SmartAtaAttribute {
|
||||
if strings.ToUpper(sa.WhenFailed) == pkg.SmartWhenFailedFailingNow {
|
||||
if strings.ToUpper(sa.WhenFailed) == pkg.AttributeWhenFailedFailingNow {
|
||||
//this attribute has previously failed
|
||||
sa.Status = pkg.SmartAttributeStatusFailed
|
||||
sa.StatusReason = "Attribute is failing manufacturer SMART threshold"
|
||||
sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusFailedSmart)
|
||||
sa.StatusReason += "Attribute is failing manufacturer SMART threshold"
|
||||
//if the Smart Status is failed, we should exit early, no need to look at thresholds.
|
||||
return sa
|
||||
|
||||
} else if strings.ToUpper(sa.WhenFailed) == pkg.SmartWhenFailedInThePast {
|
||||
sa.Status = pkg.SmartAttributeStatusWarning
|
||||
sa.StatusReason = "Attribute has previously failed manufacturer SMART threshold"
|
||||
} else if strings.ToUpper(sa.WhenFailed) == pkg.AttributeWhenFailedInThePast {
|
||||
sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusWarningScrutiny)
|
||||
sa.StatusReason += "Attribute has previously failed manufacturer SMART threshold"
|
||||
}
|
||||
|
||||
if smartMetadata, ok := thresholds.AtaMetadata[sa.AttributeId]; ok {
|
||||
@@ -138,16 +138,16 @@ func (sa *SmartAtaAttribute) ValidateThreshold(smartMetadata thresholds.AtaAttri
|
||||
|
||||
if smartMetadata.Critical {
|
||||
if obsThresh.AnnualFailureRate >= 0.10 {
|
||||
sa.Status = pkg.SmartAttributeStatusFailed
|
||||
sa.StatusReason = "Observed Failure Rate for Critical Attribute is greater than 10%"
|
||||
sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusFailedScrutiny)
|
||||
sa.StatusReason += "Observed Failure Rate for Critical Attribute is greater than 10%"
|
||||
}
|
||||
} else {
|
||||
if obsThresh.AnnualFailureRate >= 0.20 {
|
||||
sa.Status = pkg.SmartAttributeStatusFailed
|
||||
sa.StatusReason = "Observed Failure Rate for Attribute is greater than 20%"
|
||||
sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusFailedScrutiny)
|
||||
sa.StatusReason += "Observed Failure Rate for Non-Critical Attribute is greater than 20%"
|
||||
} else if obsThresh.AnnualFailureRate >= 0.10 {
|
||||
sa.Status = pkg.SmartAttributeStatusWarning
|
||||
sa.StatusReason = "Observed Failure Rate for Attribute is greater than 10%"
|
||||
sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusWarningScrutiny)
|
||||
sa.StatusReason += "Observed Failure Rate for Non-Critical Attribute is greater than 10%"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ func (sa *SmartAtaAttribute) ValidateThreshold(smartMetadata thresholds.AtaAttri
|
||||
}
|
||||
// no bucket found
|
||||
if smartMetadata.Critical {
|
||||
sa.Status = pkg.SmartAttributeStatusWarning
|
||||
sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusWarningScrutiny)
|
||||
sa.StatusReason = "Could not determine Observed Failure Rate for Critical Attribute"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package measurements
|
||||
|
||||
import "github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||
|
||||
type SmartAttribute interface {
|
||||
Flatten() (fields map[string]interface{})
|
||||
Inflate(key string, val interface{})
|
||||
GetStatus() int64
|
||||
GetStatus() pkg.AttributeStatus
|
||||
}
|
||||
|
||||
@@ -12,13 +12,13 @@ type SmartNvmeAttribute struct {
|
||||
Value int64 `json:"value"`
|
||||
Threshold int64 `json:"thresh"`
|
||||
|
||||
TransformedValue int64 `json:"transformed_value"`
|
||||
Status int64 `json:"status"`
|
||||
StatusReason string `json:"status_reason,omitempty"`
|
||||
FailureRate float64 `json:"failure_rate,omitempty"`
|
||||
TransformedValue int64 `json:"transformed_value"`
|
||||
Status pkg.AttributeStatus `json:"status"`
|
||||
StatusReason string `json:"status_reason,omitempty"`
|
||||
FailureRate float64 `json:"failure_rate,omitempty"`
|
||||
}
|
||||
|
||||
func (sa *SmartNvmeAttribute) GetStatus() int64 {
|
||||
func (sa *SmartNvmeAttribute) GetStatus() pkg.AttributeStatus {
|
||||
return sa.Status
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ func (sa *SmartNvmeAttribute) Flatten() map[string]interface{} {
|
||||
|
||||
//Generated Data
|
||||
fmt.Sprintf("attr.%s.transformed_value", sa.AttributeId): sa.TransformedValue,
|
||||
fmt.Sprintf("attr.%s.status", sa.AttributeId): sa.Status,
|
||||
fmt.Sprintf("attr.%s.status", sa.AttributeId): int64(sa.Status),
|
||||
fmt.Sprintf("attr.%s.status_reason", sa.AttributeId): sa.StatusReason,
|
||||
fmt.Sprintf("attr.%s.failure_rate", sa.AttributeId): sa.FailureRate,
|
||||
}
|
||||
@@ -54,7 +54,7 @@ func (sa *SmartNvmeAttribute) Inflate(key string, val interface{}) {
|
||||
case "transformed_value":
|
||||
sa.TransformedValue = val.(int64)
|
||||
case "status":
|
||||
sa.Status = val.(int64)
|
||||
sa.Status = pkg.AttributeStatus(val.(int64))
|
||||
case "status_reason":
|
||||
sa.StatusReason = val.(string)
|
||||
case "failure_rate":
|
||||
@@ -72,8 +72,8 @@ func (sa *SmartNvmeAttribute) PopulateAttributeStatus() *SmartNvmeAttribute {
|
||||
//check what the ideal is. Ideal tells us if we our recorded value needs to be above, or below the threshold
|
||||
if (smartMetadata.Ideal == "low" && sa.Value > sa.Threshold) ||
|
||||
(smartMetadata.Ideal == "high" && sa.Value < sa.Threshold) {
|
||||
sa.Status = pkg.SmartAttributeStatusFailed
|
||||
sa.StatusReason = "Attribute is failing recommended SMART threshold"
|
||||
sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusFailedScrutiny)
|
||||
sa.StatusReason += "Attribute is failing recommended SMART threshold"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,13 +12,13 @@ type SmartScsiAttribute struct {
|
||||
Value int64 `json:"value"`
|
||||
Threshold int64 `json:"thresh"`
|
||||
|
||||
TransformedValue int64 `json:"transformed_value"`
|
||||
Status int64 `json:"status"`
|
||||
StatusReason string `json:"status_reason,omitempty"`
|
||||
FailureRate float64 `json:"failure_rate,omitempty"`
|
||||
TransformedValue int64 `json:"transformed_value"`
|
||||
Status pkg.AttributeStatus `json:"status"`
|
||||
StatusReason string `json:"status_reason,omitempty"`
|
||||
FailureRate float64 `json:"failure_rate,omitempty"`
|
||||
}
|
||||
|
||||
func (sa *SmartScsiAttribute) GetStatus() int64 {
|
||||
func (sa *SmartScsiAttribute) GetStatus() pkg.AttributeStatus {
|
||||
return sa.Status
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ func (sa *SmartScsiAttribute) Flatten() map[string]interface{} {
|
||||
|
||||
//Generated Data
|
||||
fmt.Sprintf("attr.%s.transformed_value", sa.AttributeId): sa.TransformedValue,
|
||||
fmt.Sprintf("attr.%s.status", sa.AttributeId): sa.Status,
|
||||
fmt.Sprintf("attr.%s.status", sa.AttributeId): int64(sa.Status),
|
||||
fmt.Sprintf("attr.%s.status_reason", sa.AttributeId): sa.StatusReason,
|
||||
fmt.Sprintf("attr.%s.failure_rate", sa.AttributeId): sa.FailureRate,
|
||||
}
|
||||
@@ -54,7 +54,7 @@ func (sa *SmartScsiAttribute) Inflate(key string, val interface{}) {
|
||||
case "transformed_value":
|
||||
sa.TransformedValue = val.(int64)
|
||||
case "status":
|
||||
sa.Status = val.(int64)
|
||||
sa.Status = pkg.AttributeStatus(val.(int64))
|
||||
case "status_reason":
|
||||
sa.StatusReason = val.(string)
|
||||
case "failure_rate":
|
||||
@@ -73,7 +73,7 @@ func (sa *SmartScsiAttribute) PopulateAttributeStatus() *SmartScsiAttribute {
|
||||
//check what the ideal is. Ideal tells us if we our recorded value needs to be above, or below the threshold
|
||||
if (smartMetadata.Ideal == "low" && sa.Value > sa.Threshold) ||
|
||||
(smartMetadata.Ideal == "high" && sa.Value < sa.Threshold) {
|
||||
sa.Status = pkg.SmartAttributeStatusFailed
|
||||
sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusFailedScrutiny)
|
||||
sa.StatusReason = "Attribute is failing recommended SMART threshold"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,9 +328,12 @@ func TestFromCollectorSmartInfo(t *testing.T) {
|
||||
require.Equal(t, 18, len(smartMdl.Attributes))
|
||||
|
||||
//check that temperature was correctly parsed
|
||||
|
||||
require.Equal(t, int64(163210330144), smartMdl.Attributes["194"].(*measurements.SmartAtaAttribute).RawValue)
|
||||
require.Equal(t, int64(32), smartMdl.Attributes["194"].(*measurements.SmartAtaAttribute).TransformedValue)
|
||||
|
||||
//ensure that Scrutiny warning for a non critical attribute does not set device status to failed.
|
||||
require.Equal(t, pkg.AttributeStatusWarningScrutiny, smartMdl.Attributes["3"].GetStatus())
|
||||
|
||||
}
|
||||
|
||||
func TestFromCollectorSmartInfo_Fail_Smart(t *testing.T) {
|
||||
@@ -402,7 +405,7 @@ func TestFromCollectorSmartInfo_Fail_ScrutinyNonCriticalFailed(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "WWN-test", smartMdl.DeviceWWN)
|
||||
require.Equal(t, pkg.DeviceStatusFailedScrutiny, smartMdl.Status)
|
||||
require.Equal(t, int64(pkg.SmartAttributeStatusFailed), smartMdl.Attributes["199"].GetStatus(),
|
||||
require.Equal(t, pkg.AttributeStatusFailedScrutiny, smartMdl.Attributes["199"].GetStatus(),
|
||||
"scrutiny should detect that %d failed (status: %d, %s)",
|
||||
smartMdl.Attributes["199"].(*measurements.SmartAtaAttribute).AttributeId,
|
||||
smartMdl.Attributes["199"].GetStatus(), smartMdl.Attributes["199"].(*measurements.SmartAtaAttribute).StatusReason,
|
||||
@@ -435,7 +438,7 @@ func TestFromCollectorSmartInfo_NVMe_Fail_Scrutiny(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "WWN-test", smartMdl.DeviceWWN)
|
||||
require.Equal(t, pkg.DeviceStatusFailedScrutiny, smartMdl.Status)
|
||||
require.Equal(t, int64(pkg.SmartAttributeStatusFailed), smartMdl.Attributes["media_errors"].GetStatus(),
|
||||
require.Equal(t, pkg.AttributeStatusFailedScrutiny, smartMdl.Attributes["media_errors"].GetStatus(),
|
||||
"scrutiny should detect that %s failed (status: %d, %s)",
|
||||
smartMdl.Attributes["media_errors"].(*measurements.SmartNvmeAttribute).AttributeId,
|
||||
smartMdl.Attributes["media_errors"].GetStatus(),
|
||||
|
||||
@@ -445,50 +445,6 @@ var AtaMetadata = map[int]AtaAttributeMetadata{
|
||||
Ideal: ObservedThresholdIdealLow,
|
||||
Critical: false,
|
||||
Description: "This attribute indicates the count of full hard disk power on/off cycles.",
|
||||
ObservedThresholds: []ObservedThreshold{
|
||||
{
|
||||
Low: 0,
|
||||
High: 13,
|
||||
AnnualFailureRate: 0.019835987118930823,
|
||||
ErrorInterval: []float64{0.016560870164523494, 0.023569242386797896},
|
||||
},
|
||||
{
|
||||
Low: 13,
|
||||
High: 26,
|
||||
AnnualFailureRate: 0.038210930067894826,
|
||||
ErrorInterval: []float64{0.03353859179329295, 0.0433520775718649},
|
||||
},
|
||||
{
|
||||
Low: 26,
|
||||
High: 39,
|
||||
AnnualFailureRate: 0.11053528307302571,
|
||||
ErrorInterval: []float64{0.09671061589521368, 0.1257816678419765},
|
||||
},
|
||||
{
|
||||
Low: 39,
|
||||
High: 52,
|
||||
AnnualFailureRate: 0.16831189443375036,
|
||||
ErrorInterval: []float64{0.1440976510675928, 0.19543066007594895},
|
||||
},
|
||||
{
|
||||
Low: 52,
|
||||
High: 65,
|
||||
AnnualFailureRate: 0.20630344262550107,
|
||||
ErrorInterval: []float64{0.1693965932069108, 0.2488633537247856},
|
||||
},
|
||||
{
|
||||
Low: 65,
|
||||
High: 78,
|
||||
AnnualFailureRate: 0.1030972634140512,
|
||||
ErrorInterval: []float64{0.06734655535304743, 0.15106137807407605},
|
||||
},
|
||||
{
|
||||
Low: 78,
|
||||
High: 91,
|
||||
AnnualFailureRate: 0.12354840389522469,
|
||||
ErrorInterval: []float64{0.06578432170016109, 0.21127153335749593},
|
||||
},
|
||||
},
|
||||
},
|
||||
13: {
|
||||
ID: 13,
|
||||
|
||||
@@ -2,4 +2,4 @@ package version
|
||||
|
||||
// VERSION is the app-global version string, which will be replaced with a
|
||||
// new value during packaging
|
||||
const VERSION = "0.4.6"
|
||||
const VERSION = "0.4.9"
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func DeleteDevice(c *gin.Context) {
|
||||
logger := c.MustGet("LOGGER").(logrus.FieldLogger)
|
||||
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
|
||||
|
||||
err := deviceRepo.DeleteDevice(c, c.Param("wwn"))
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while deleting device", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type AppEngine struct {
|
||||
@@ -47,6 +48,8 @@ func (ae *AppEngine) Setup(logger logrus.FieldLogger) *gin.Engine {
|
||||
api.POST("/device/:wwn/smart", handler.UploadDeviceMetrics) //used by Collector to upload data
|
||||
api.POST("/device/:wwn/selftest", handler.UploadDeviceSelfTests)
|
||||
api.GET("/device/:wwn/details", handler.GetDeviceDetails) //used by Details
|
||||
api.DELETE("/device/:wwn", handler.DeleteDevice) //used by UI to delete device
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +69,11 @@ func (ae *AppEngine) Setup(logger logrus.FieldLogger) *gin.Engine {
|
||||
}
|
||||
|
||||
func (ae *AppEngine) Start() error {
|
||||
//set the gin mode
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
if strings.ToLower(ae.Config.GetString("log.level")) == "debug" {
|
||||
gin.SetMode(gin.DebugMode)
|
||||
}
|
||||
|
||||
logger := logrus.New()
|
||||
//set default log level
|
||||
|
||||
@@ -93,6 +93,7 @@ func (suite *ServerTestSuite) TestHealthRoute() {
|
||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").Return(parentPath).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
|
||||
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
|
||||
@@ -132,6 +133,7 @@ func (suite *ServerTestSuite) TestRegisterDevicesRoute() {
|
||||
fakeConfig.EXPECT().GetString("web.database.location").Return(path.Join(parentPath, "scrutiny_test.db")).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").Return(parentPath).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
|
||||
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
|
||||
@@ -171,6 +173,7 @@ func (suite *ServerTestSuite) TestUploadDeviceMetricsRoute() {
|
||||
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
|
||||
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
|
||||
@@ -219,6 +222,7 @@ func (suite *ServerTestSuite) TestPopulateMultiple() {
|
||||
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
|
||||
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
|
||||
@@ -314,6 +318,7 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_WebhookFailure() {
|
||||
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
|
||||
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
|
||||
@@ -352,6 +357,7 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptFailure() {
|
||||
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
|
||||
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
|
||||
@@ -390,6 +396,7 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptSuccess() {
|
||||
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
|
||||
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
|
||||
@@ -428,6 +435,7 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ShoutrrrFailure() {
|
||||
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
|
||||
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
|
||||
@@ -465,6 +473,7 @@ func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() {
|
||||
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
|
||||
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
|
||||
@@ -509,5 +518,5 @@ func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() {
|
||||
|
||||
//assert
|
||||
require.Equal(suite.T(), "a4c8e8ed-11a0-4c97-9bba-306440f1b944", deviceSummary.Data.Summary["a4c8e8ed-11a0-4c97-9bba-306440f1b944"].Device.WWN)
|
||||
require.Equal(suite.T(), pkg.DeviceStatusFailedScrutiny, deviceSummary.Data.Summary["a4c8e8ed-11a0-4c97-9bba-306440f1b944"].Device.DeviceStatus)
|
||||
require.Equal(suite.T(), pkg.DeviceStatusPassed, deviceSummary.Data.Summary["a4c8e8ed-11a0-4c97-9bba-306440f1b944"].Device.DeviceStatus)
|
||||
}
|
||||
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if [[ -z "${CI}" ]]; then
|
||||
echo "running locally (not in Github Actions). generating version file from git client"
|
||||
GIT_TAG=`git describe --tags`
|
||||
GIT_BRANCH=`git rev-parse --abbrev-ref HEAD`
|
||||
|
||||
if [[ "$GIT_BRANCH" == "master" ]]; then
|
||||
VERSION_INFO="${GIT_TAG}"
|
||||
else
|
||||
VERSION_INFO="${GIT_BRANCH}#${GIT_TAG}"
|
||||
fi
|
||||
else
|
||||
echo "running in Github Actions, generating version file from environmental variables"
|
||||
# https://docs.github.com/en/actions/learn-github-actions/environment-variables
|
||||
VERSION_INFO="${GITHUB_REF_NAME}"
|
||||
|
||||
if [[ "$GITHUB_REF_TYPE" == "branch" ]]; then
|
||||
VERSION_INFO="${VERSION_INFO}#${GITHUB_SHA::7}"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "writing version file (version: ${VERSION_INFO})"
|
||||
cat <<EOT > src/environments/versions.ts
|
||||
// this file is automatically generated by git.version.ts script
|
||||
export const versionInfo = {
|
||||
version: '${VERSION_INFO}',
|
||||
};
|
||||
EOT
|
||||
@@ -2,6 +2,9 @@ import { Inject, Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import * as _ from 'lodash';
|
||||
import { TREO_APP_CONFIG } from '@treo/services/config/config.constants';
|
||||
import { AppConfig } from 'app/core/config/app.config';
|
||||
|
||||
const SCRUTINY_CONFIG_LOCAL_STORAGE_KEY = 'scrutiny';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@@ -14,10 +17,18 @@ export class TreoConfigService
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(@Inject(TREO_APP_CONFIG) config: any)
|
||||
constructor(@Inject(TREO_APP_CONFIG) defaultConfig: any)
|
||||
{
|
||||
let currentScrutinyConfig = defaultConfig
|
||||
|
||||
let localConfigStr = localStorage.getItem(SCRUTINY_CONFIG_LOCAL_STORAGE_KEY)
|
||||
if (localConfigStr){
|
||||
//check localstorage for a value
|
||||
let localConfig = JSON.parse(localConfigStr)
|
||||
currentScrutinyConfig = Object.assign({}, localConfig, currentScrutinyConfig) // make sure defaults are available if missing from localStorage.
|
||||
}
|
||||
// Set the private defaults
|
||||
this._config = new BehaviorSubject(config);
|
||||
this._config = new BehaviorSubject(currentScrutinyConfig);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
@@ -27,20 +38,29 @@ export class TreoConfigService
|
||||
/**
|
||||
* Setter and getter for config
|
||||
*/
|
||||
//Setter
|
||||
set config(value: any)
|
||||
{
|
||||
// Merge the new config over to the current config
|
||||
const config = _.merge({}, this._config.getValue(), value);
|
||||
let config = _.merge({}, this._config.getValue(), value);
|
||||
|
||||
//Store the config in localstorage
|
||||
localStorage.setItem(SCRUTINY_CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config));
|
||||
|
||||
// Execute the observable
|
||||
this._config.next(config);
|
||||
}
|
||||
|
||||
//Getter
|
||||
get config$(): Observable<any>
|
||||
{
|
||||
return this._config.asObservable();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Private methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Layout } from "app/layout/layout.types";
|
||||
|
||||
// Theme type
|
||||
export type Theme = "light" | "dark";
|
||||
export type Theme = "light" | "dark" | "system";
|
||||
|
||||
/**
|
||||
* AppConfig interface. Update this interface to strictly type your config
|
||||
@@ -11,6 +11,12 @@ export interface AppConfig
|
||||
{
|
||||
theme: Theme;
|
||||
layout: Layout;
|
||||
|
||||
// Dashboard options
|
||||
dashboardDisplay: string;
|
||||
dashboardSort: string;
|
||||
|
||||
temperatureUnit: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -23,6 +29,11 @@ export interface AppConfig
|
||||
*/
|
||||
export const appConfig: AppConfig = {
|
||||
theme : "light",
|
||||
layout: "material"
|
||||
layout: "material",
|
||||
|
||||
dashboardDisplay: "name",
|
||||
dashboardSort: "status",
|
||||
|
||||
temperatureUnit: "celsius",
|
||||
};
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@ export const summary = {
|
||||
"DeletedAt": null,
|
||||
"wwn": "0x5000c500673e6b5f",
|
||||
"device_name": "sdg",
|
||||
"device_label": "14TB-WD-DRIVE2",
|
||||
"device_uuid": "",
|
||||
"device_serial_id": "ata-ST6000DX000-1H217Z-Z4DXXXXX",
|
||||
"manufacturer": "ATA",
|
||||
"model_name": "ST6000DX000-1H217Z",
|
||||
"interface_type": "SCSI",
|
||||
@@ -35,6 +38,9 @@ export const summary = {
|
||||
"DeletedAt": null,
|
||||
"wwn": "0x5000cca252c859cc",
|
||||
"device_name": "sdd",
|
||||
"device_label": "14TB-WD-DRIVE1",
|
||||
"device_uuid": "806cf4bc-d160-4d96-8ee9-3ab7cf2a2e1f",
|
||||
"device_serial_id": "ata-WDC_WD80EFAX-68LHPN0-7SGLXXXXX",
|
||||
"manufacturer": "ATA",
|
||||
"model_name": "WDC_WD80EFAX-68LHPN0",
|
||||
"interface_type": "SCSI",
|
||||
@@ -68,6 +74,9 @@ export const summary = {
|
||||
"DeletedAt": null,
|
||||
"wwn": "0x5000cca264eb01d7",
|
||||
"device_name": "sdb",
|
||||
"device_label": "14TB-WD-DRIVE5",
|
||||
"device_uuid": "8125ec6d-a7e4-4950-ac84-72d6a4d67128",
|
||||
"device_serial_id": "ata-WDC_WD140EDFZ-11A0VA0-9RK1XXXXX",
|
||||
"manufacturer": "ATA",
|
||||
"model_name": "WDC_WD140EDFZ-11A0VA0",
|
||||
"interface_type": "SCSI",
|
||||
@@ -101,6 +110,9 @@ export const summary = {
|
||||
"DeletedAt": null,
|
||||
"wwn": "0x5000cca264ebc248",
|
||||
"device_name": "sde",
|
||||
"device_label": "14TB-WD-DRIVE3",
|
||||
"device_uuid": "9eb60cde-d6d0-4172-b520-b241a6a5477f",
|
||||
"device_serial_id": "ata-WDC_WD140EDFZ-11A0VA0-9RK3XXXXX",
|
||||
"manufacturer": "ATA",
|
||||
"model_name": "WDC_WD140EDFZ-11A0VA0",
|
||||
"interface_type": "SCSI",
|
||||
@@ -125,6 +137,9 @@ export const summary = {
|
||||
"DeletedAt": null,
|
||||
"wwn": "0x5000cca264ec3183",
|
||||
"device_name": "sdc",
|
||||
"device_label": "14TB-WD-DRIVE6",
|
||||
"device_uuid": "e1378723-7861-49b9-8e01-0bd063f0ecdd",
|
||||
"device_serial_id": "ata-WDC_WD140EDFZ-11A0VA0-9RK4XXXXX",
|
||||
"manufacturer": "ATA",
|
||||
"model_name": "WDC_WD140EDFZ-11A0VA0",
|
||||
"interface_type": "SCSI",
|
||||
@@ -138,7 +153,7 @@ export const summary = {
|
||||
"device_protocol": "",
|
||||
"device_type": "",
|
||||
"label": "",
|
||||
"host_id": "",
|
||||
"host_id": "custom host id",
|
||||
"device_status": 1
|
||||
},
|
||||
"smart": {
|
||||
@@ -542,6 +557,9 @@ export const summary = {
|
||||
"DeletedAt": null,
|
||||
"wwn": "0x50014ee20b2a72a9",
|
||||
"device_name": "sdf",
|
||||
"device_label": "8.0TB-WD-4",
|
||||
"device_uuid": "fc684dcc-aa2f-44f3-a958-d302dc7dd46d",
|
||||
"device_serial_id": "ata-WDC_WD60EFRX-68MYMN1-WXL1HXXXXX",
|
||||
"manufacturer": "ATA",
|
||||
"model_name": "WDC_WD60EFRX-68MYMN1",
|
||||
"interface_type": "SCSI",
|
||||
@@ -566,6 +584,9 @@ export const summary = {
|
||||
"DeletedAt": null,
|
||||
"wwn": "0x5002538e40a22954",
|
||||
"device_name": "sda",
|
||||
"device_label": "",
|
||||
"device_uuid": "",
|
||||
"device_serial_id": "ata-Samsung_SSD_860_EVO_500GB-S3YZNB0KBXXXXXX",
|
||||
"manufacturer": "ATA",
|
||||
"model_name": "Samsung_SSD_860_EVO_500GB",
|
||||
"interface_type": "SCSI",
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<h2 mat-dialog-title>Delete {{data.title}}?</h2>
|
||||
<mat-dialog-content>This will delete all data associated with this device (including all historical data).</mat-dialog-content>
|
||||
<mat-dialog-actions>
|
||||
<button mat-button mat-dialog-close>Cancel</button>
|
||||
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
|
||||
<button class="red-600" mat-button (click)="onDeleteClick()">
|
||||
<mat-icon class="icon-size-20 mr-3"
|
||||
[svgIcon]="'delete_forever'"></mat-icon>
|
||||
Delete</button>
|
||||
</mat-dialog-actions>
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DashboardDeviceDeleteDialogComponent } from './dashboard-device-delete-dialog.component';
|
||||
|
||||
describe('DashboardDeviceDeleteDialogComponent', () => {
|
||||
let component: DashboardDeviceDeleteDialogComponent;
|
||||
let fixture: ComponentFixture<DashboardDeviceDeleteDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ DashboardDeviceDeleteDialogComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DashboardDeviceDeleteDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { Component, OnInit, Inject } from '@angular/core';
|
||||
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
|
||||
import {DashboardDeviceDeleteDialogService} from "./dashboard-device-delete-dialog.service";
|
||||
import {Subject} from "rxjs";
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard-device-delete-dialog',
|
||||
templateUrl: './dashboard-device-delete-dialog.component.html',
|
||||
styleUrls: ['./dashboard-device-delete-dialog.component.scss']
|
||||
})
|
||||
export class DashboardDeviceDeleteDialogComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<DashboardDeviceDeleteDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: {wwn: string, title: string},
|
||||
private _deleteService: DashboardDeviceDeleteDialogService,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
onDeleteClick(): void {
|
||||
this._deleteService.deleteDevice(this.data.wwn)
|
||||
.subscribe((data) => {
|
||||
this.dialogRef.close(data);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { Overlay } from '@angular/cdk/overlay';
|
||||
import { MAT_AUTOCOMPLETE_SCROLL_STRATEGY, MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { SharedModule } from 'app/shared/shared.module';
|
||||
import {DashboardDeviceDeleteDialogComponent} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component'
|
||||
import { MatButtonToggleModule} from "@angular/material/button-toggle";
|
||||
import {MatTabsModule} from "@angular/material/tabs";
|
||||
import {MatSliderModule} from "@angular/material/slider";
|
||||
import {MatSlideToggleModule} from "@angular/material/slide-toggle";
|
||||
import {MatTooltipModule} from "@angular/material/tooltip";
|
||||
import {dashboardRoutes} from "../../../modules/dashboard/dashboard.routing";
|
||||
import {MatDividerModule} from "@angular/material/divider";
|
||||
import {MatMenuModule} from "@angular/material/menu";
|
||||
import {MatProgressBarModule} from "@angular/material/progress-bar";
|
||||
import {MatSortModule} from "@angular/material/sort";
|
||||
import {MatTableModule} from "@angular/material/table";
|
||||
import {NgApexchartsModule} from "ng-apexcharts";
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
DashboardDeviceDeleteDialogComponent
|
||||
],
|
||||
imports : [
|
||||
RouterModule.forChild([]),
|
||||
RouterModule.forChild(dashboardRoutes),
|
||||
MatButtonModule,
|
||||
MatDividerModule,
|
||||
MatTooltipModule,
|
||||
MatIconModule,
|
||||
MatMenuModule,
|
||||
MatProgressBarModule,
|
||||
MatSortModule,
|
||||
MatTableModule,
|
||||
NgApexchartsModule,
|
||||
SharedModule,
|
||||
MatDialogModule
|
||||
],
|
||||
exports : [
|
||||
DashboardDeviceDeleteDialogComponent,
|
||||
],
|
||||
providers : []
|
||||
})
|
||||
export class DashboardDeviceDeleteDialogModule
|
||||
{
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { getBasePath } from 'app/app.routing';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DashboardDeviceDeleteDialogService
|
||||
{
|
||||
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {HttpClient} _httpClient
|
||||
*/
|
||||
constructor(
|
||||
private _httpClient: HttpClient
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
deleteDevice(wwn: string): Observable<any>
|
||||
{
|
||||
return this._httpClient.delete( `${getBasePath()}/api/device/${wwn}`, {});
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
<div [ngClass]="{ 'border-green': deviceSummary.device.device_status == 0 && deviceSummary.smart,
|
||||
'border-red': deviceSummary.device.device_status != 0 }"
|
||||
class="relative flex flex-col flex-auto p-6 pr-3 pb-3 bg-card rounded border-l-4 shadow-md overflow-hidden">
|
||||
<div class="absolute bottom-0 right-0 w-24 h-24 -m-6">
|
||||
<mat-icon class="icon-size-96 opacity-12 text-green"
|
||||
*ngIf="deviceSummary.device.device_status == 0 && deviceSummary.smart"
|
||||
[svgIcon]="'heroicons_outline:check-circle'"></mat-icon>
|
||||
<mat-icon class="icon-size-96 opacity-12 text-red"
|
||||
*ngIf="deviceSummary.device.device_status != 0"
|
||||
[svgIcon]="'heroicons_outline:exclamation-circle'"></mat-icon>
|
||||
<mat-icon class="icon-size-96 opacity-12 text-yellow"
|
||||
*ngIf="!deviceSummary.smart"
|
||||
[svgIcon]="'heroicons_outline:question-mark-circle'"></mat-icon>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="flex flex-col">
|
||||
<a [routerLink]="'/device/'+ deviceSummary.device.wwn"
|
||||
class="font-bold text-md text-secondary uppercase tracking-wider">{{deviceSummary.device | deviceTitle:config.dashboardDisplay}}</a>
|
||||
<div [ngClass]="classDeviceLastUpdatedOn(deviceSummary)" class="font-medium text-sm" *ngIf="deviceSummary.smart">
|
||||
Last Updated on {{deviceSummary.smart.collector_date | date:'MMMM dd, yyyy - HH:mm' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-auto" *ngIf="deviceSummary.device">
|
||||
<button mat-icon-button
|
||||
[matMenuTriggerFor]="previousStatementMenu">
|
||||
<mat-icon [svgIcon]="'more_vert'"></mat-icon>
|
||||
</button>
|
||||
<mat-menu #previousStatementMenu="matMenu">
|
||||
<a mat-menu-item [routerLink]="'/device/'+ deviceSummary.device.wwn">
|
||||
<span class="flex items-center">
|
||||
<mat-icon class="icon-size-20 mr-3"
|
||||
[svgIcon]="'assessment'"></mat-icon>
|
||||
<span>View Details</span>
|
||||
</span>
|
||||
</a>
|
||||
<a mat-menu-item (click)="openDeleteDialog()">
|
||||
<span class="flex items-center">
|
||||
<mat-icon class="icon-size-20 mr-3"
|
||||
[svgIcon]="'delete_forever'"></mat-icon>
|
||||
<span>Delete Device</span>
|
||||
</span>
|
||||
</a>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row flex-wrap mt-4 -mx-6">
|
||||
<div class="flex flex-col mx-6 my-3 xs:w-full">
|
||||
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Status</div>
|
||||
<div class="mt-2 font-medium text-3xl leading-none" *ngIf="deviceSummary.smart?.collector_date; else unknownStatus">{{ deviceStatusString(deviceSummary.device.device_status) | titlecase}}</div>
|
||||
<ng-template #unknownStatus><div class="mt-2 font-medium text-3xl leading-none">No Data</div></ng-template>
|
||||
</div>
|
||||
<div class="flex flex-col mx-6 my-3 xs:w-full">
|
||||
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Temperature</div>
|
||||
<div class="mt-2 font-medium text-3xl leading-none" *ngIf="deviceSummary.smart?.collector_date; else unknownTemp">{{ deviceSummary.smart?.temp | temperature:config.temperatureUnit:true }}</div>
|
||||
<ng-template #unknownTemp><div class="mt-2 font-medium text-3xl leading-none">--</div></ng-template>
|
||||
</div>
|
||||
<div class="flex flex-col mx-6 my-3 xs:w-full">
|
||||
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Capacity</div>
|
||||
<div class="mt-2 font-medium text-3xl leading-none">{{ deviceSummary.device.capacity | fileSize}}</div>
|
||||
</div>
|
||||
<div class="flex flex-col mx-6 my-3 xs:w-full">
|
||||
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Powered On</div>
|
||||
<div class="mt-2 font-medium text-3xl leading-none" *ngIf="deviceSummary.smart?.power_on_hours; else unknownPoweredOn">{{ humanizeDuration(deviceSummary.smart?.power_on_hours * 60 * 60 * 1000, { round: true, largest: 1, units: ['y', 'd', 'h'] }) }}</div>
|
||||
<ng-template #unknownPoweredOn><div class="mt-2 font-medium text-3xl leading-none">--</div></ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DashboardDeviceComponent } from './dashboard-device.component';
|
||||
|
||||
describe('DashboardDeviceComponent', () => {
|
||||
let component: DashboardDeviceComponent;
|
||||
let fixture: ComponentFixture<DashboardDeviceComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ DashboardDeviceComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DashboardDeviceComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
import { Component, Input, Output, OnInit, EventEmitter} from '@angular/core';
|
||||
import * as moment from "moment";
|
||||
import {takeUntil} from "rxjs/operators";
|
||||
import {AppConfig} from "app/core/config/app.config";
|
||||
import {TreoConfigService} from "@treo/services/config";
|
||||
import {Subject} from "rxjs";
|
||||
import humanizeDuration from 'humanize-duration'
|
||||
import {MatDialog} from '@angular/material/dialog';
|
||||
import {DashboardDeviceDeleteDialogComponent} from "app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component";
|
||||
import {DeviceTitlePipe} from "app/shared/device-title.pipe";
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard-device',
|
||||
templateUrl: './dashboard-device.component.html',
|
||||
styleUrls: ['./dashboard-device.component.scss']
|
||||
})
|
||||
export class DashboardDeviceComponent implements OnInit {
|
||||
@Input() deviceSummary: any;
|
||||
@Input() deviceWWN: string;
|
||||
@Output() deviceDeleted = new EventEmitter<string>();
|
||||
|
||||
config: AppConfig;
|
||||
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
|
||||
constructor(
|
||||
private _configService: TreoConfigService,
|
||||
public dialog: MatDialog,
|
||||
) {
|
||||
// Set the private defaults
|
||||
this._unsubscribeAll = new Subject();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to config changes
|
||||
this._configService.config$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((config: AppConfig) => {
|
||||
this.config = config;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
classDeviceLastUpdatedOn(deviceSummary){
|
||||
if (deviceSummary.device.device_status !== 0) {
|
||||
return 'text-red' // if the device has failed, always highlight in red
|
||||
} else if(deviceSummary.device.device_status === 0 && deviceSummary.smart){
|
||||
if(moment().subtract(14, 'd').isBefore(deviceSummary.smart.collector_date)){
|
||||
// this device was updated in the last 2 weeks.
|
||||
return 'text-green'
|
||||
} else if(moment().subtract(1, 'm').isBefore(deviceSummary.smart.collector_date)){
|
||||
// this device was updated in the last month
|
||||
return 'text-yellow'
|
||||
} else{
|
||||
// last updated more than a month ago.
|
||||
return 'text-red'
|
||||
}
|
||||
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
deviceStatusString(deviceStatus){
|
||||
if(deviceStatus == 0){
|
||||
return "passed"
|
||||
} else {
|
||||
return "failed"
|
||||
}
|
||||
}
|
||||
|
||||
readonly humanizeDuration = humanizeDuration;
|
||||
|
||||
|
||||
|
||||
openDeleteDialog(): void {
|
||||
const dialogRef = this.dialog.open(DashboardDeviceDeleteDialogComponent, {
|
||||
// width: '250px',
|
||||
data: {wwn: this.deviceWWN, title: DeviceTitlePipe.deviceTitleWithFallback(this.deviceSummary.device, this.config.dashboardDisplay)}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
console.log('The dialog was closed', result);
|
||||
if(result.success){
|
||||
this.deviceDeleted.emit(this.deviceWWN)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { Overlay } from '@angular/cdk/overlay';
|
||||
import { MAT_AUTOCOMPLETE_SCROLL_STRATEGY, MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { SharedModule } from 'app/shared/shared.module';
|
||||
import {DashboardDeviceComponent} from 'app/layout/common/dashboard-device/dashboard-device.component'
|
||||
import { MatDialogModule } from "@angular/material/dialog";
|
||||
import { MatButtonToggleModule} from "@angular/material/button-toggle";
|
||||
import {MatTabsModule} from "@angular/material/tabs";
|
||||
import {MatSliderModule} from "@angular/material/slider";
|
||||
import {MatSlideToggleModule} from "@angular/material/slide-toggle";
|
||||
import {MatTooltipModule} from "@angular/material/tooltip";
|
||||
import {dashboardRoutes} from "../../../modules/dashboard/dashboard.routing";
|
||||
import {MatDividerModule} from "@angular/material/divider";
|
||||
import {MatMenuModule} from "@angular/material/menu";
|
||||
import {MatProgressBarModule} from "@angular/material/progress-bar";
|
||||
import {MatSortModule} from "@angular/material/sort";
|
||||
import {MatTableModule} from "@angular/material/table";
|
||||
import {NgApexchartsModule} from "ng-apexcharts";
|
||||
import {DashboardDeviceDeleteDialogModule} from "../dashboard-device-delete-dialog/dashboard-device-delete-dialog.module";
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
DashboardDeviceComponent
|
||||
],
|
||||
imports : [
|
||||
RouterModule.forChild([]),
|
||||
RouterModule.forChild(dashboardRoutes),
|
||||
MatButtonModule,
|
||||
MatDividerModule,
|
||||
MatTooltipModule,
|
||||
MatIconModule,
|
||||
MatMenuModule,
|
||||
MatProgressBarModule,
|
||||
MatSortModule,
|
||||
MatTableModule,
|
||||
NgApexchartsModule,
|
||||
SharedModule,
|
||||
DashboardDeviceDeleteDialogModule
|
||||
],
|
||||
exports : [
|
||||
DashboardDeviceComponent,
|
||||
],
|
||||
providers : []
|
||||
})
|
||||
export class DashboardDeviceModule
|
||||
{
|
||||
}
|
||||
+63
-30
@@ -1,77 +1,110 @@
|
||||
<h2 mat-dialog-title>Scrutiny Settings</h2>
|
||||
<mat-dialog-content class="mat-typography">
|
||||
|
||||
<form class="flex flex-col p-8 pb-0 overflow-hidden">
|
||||
<div class="flex flex-col gt-xs:flex-row">
|
||||
<mat-form-field class="flex-auto gt-xs:pr-3">
|
||||
<mat-label>Sort By</mat-label>
|
||||
<mat-select [value]="'status'">
|
||||
<mat-option value="status">Status</mat-option>
|
||||
<mat-option value="name" disabled>Name</mat-option>
|
||||
<mat-option value="label" disabled>Label</mat-option>
|
||||
<div class="flex flex-col p-8 pb-0 overflow-hidden">
|
||||
<div class="flex flex-col mt-5 gt-md:flex-row">
|
||||
|
||||
<mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pr-3">
|
||||
<mat-label>Dark Mode</mat-label>
|
||||
<mat-select [(ngModel)]="theme">
|
||||
<mat-option value="system">System</mat-option>
|
||||
<mat-option value="dark">Dark</mat-option>
|
||||
<mat-option value="light">Light</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col mt-5 gt-md:flex-row">
|
||||
<mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pr-3">
|
||||
<mat-label>Display Title</mat-label>
|
||||
<mat-select [(ngModel)]="dashboardDisplay">
|
||||
<mat-option value="name">Name</mat-option>
|
||||
<mat-option value="serial_id">Serial ID</mat-option>
|
||||
<mat-option value="uuid">UUID</mat-option>
|
||||
<mat-option value="label">Label</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pl-3">
|
||||
<mat-label>Sort By</mat-label>
|
||||
<mat-select [(ngModel)]="dashboardSort">
|
||||
<mat-option value="status">Status</mat-option>
|
||||
<mat-option value="title">Title</mat-option>
|
||||
<mat-option value="age">Age</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col mt-5 gt-md:flex-row">
|
||||
<mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pr-3">
|
||||
<mat-label>Temperature Display Unit</mat-label>
|
||||
<mat-select [(ngModel)]="temperatureUnit">
|
||||
<mat-option value="celsius">Celsius</mat-option>
|
||||
<mat-option value="fahrenheit">Fahrenheit</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<mat-tab-group mat-align-tabs="start">
|
||||
<mat-tab label="Ata">
|
||||
|
||||
<div class="flex flex-col mt-5 gt-md:flex-row">
|
||||
<div matTooltip="not yet implemented" class="flex flex-col mt-5 gt-md:flex-row">
|
||||
<mat-form-field class="flex-auto gt-md:pr-3">
|
||||
<mat-label>Critical Error Threshold</mat-label>
|
||||
<input matInput [value]="'10%'">
|
||||
<mat-label class="text-hint">Critical Error Threshold</mat-label>
|
||||
<input disabled matInput [value]="'10%'">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="flex-auto gt-md:pl-3">
|
||||
<mat-label>Critical Warning Threshold</mat-label>
|
||||
<input matInput>
|
||||
<mat-label class="text-hint">Critical Warning Threshold</mat-label>
|
||||
<input disabled matInput>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gt-md:flex-row">
|
||||
<div matTooltip="not yet implemented" class="flex flex-col gt-md:flex-row">
|
||||
<mat-form-field class="flex-auto gt-md:pr-3">
|
||||
<mat-label>Error Threshold</mat-label>
|
||||
<input matInput [value]="'20%'">
|
||||
<mat-label class="text-hint">Error Threshold</mat-label>
|
||||
<input disabled matInput [value]="'20%'">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="flex-auto gt-md:pl-3">
|
||||
<mat-label>Warning Threshold</mat-label>
|
||||
<input matInput [value]="'10%'">
|
||||
<mat-label class="text-hint">Warning Threshold</mat-label>
|
||||
<input disabled matInput [value]="'10%'">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
</mat-tab>
|
||||
<mat-tab label="NVMe">
|
||||
|
||||
<div class="flex flex-col mt-5 gt-md:flex-row">
|
||||
<div matTooltip="not yet implemented" class="flex flex-col mt-5 gt-md:flex-row">
|
||||
<mat-form-field class="flex-auto gt-md:pr-3">
|
||||
<mat-label>Critical Error Threshold</mat-label>
|
||||
<input matInput [value]="'enabled'">
|
||||
<mat-label class="text-hint">Critical Error Threshold</mat-label>
|
||||
<input disabled matInput [value]="'enabled'">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="flex-auto gt-md:pl-3">
|
||||
<mat-label>Critical Warning Threshold</mat-label>
|
||||
<input matInput>
|
||||
<mat-label class="text-hint">Critical Warning Threshold</mat-label>
|
||||
<input disabled matInput>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
</mat-tab>
|
||||
<mat-tab label="SCSI">
|
||||
<div class="flex flex-col mt-5 gt-md:flex-row">
|
||||
<div matTooltip="not yet implemented" class="flex flex-col mt-5 gt-md:flex-row">
|
||||
<mat-form-field class="flex-auto gt-md:pr-3">
|
||||
<mat-label>Critical Error Threshold</mat-label>
|
||||
<input matInput [value]="'enabled'">
|
||||
<mat-label class="text-hint">Critical Error Threshold</mat-label>
|
||||
<input disabled matInput [value]="'enabled'">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="flex-auto gt-md:pl-3">
|
||||
<mat-label>Critical Warning Threshold</mat-label>
|
||||
<input matInput>
|
||||
<mat-label class="text-hint">Critical Warning Threshold</mat-label>
|
||||
<input disabled matInput>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button mat-dialog-close>Cancel</button>
|
||||
<button mat-button matTooltip="not yet implemented" [mat-dialog-close]="true" cdkFocusInitial>Save</button>
|
||||
<button mat-button mat-dialog-close (click)="saveSettings()" cdkFocusInitial>Save</button>
|
||||
</mat-dialog-actions>
|
||||
|
||||
+46
-2
@@ -1,4 +1,8 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import {AppConfig} from 'app/core/config/app.config';
|
||||
import { TreoConfigService } from '@treo/services/config';
|
||||
import {Subject} from "rxjs";
|
||||
import {takeUntil} from "rxjs/operators";
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard-settings',
|
||||
@@ -7,11 +11,51 @@ import { Component, OnInit } from '@angular/core';
|
||||
})
|
||||
export class DashboardSettingsComponent implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
dashboardDisplay: string;
|
||||
dashboardSort: string;
|
||||
temperatureUnit: string;
|
||||
theme: string;
|
||||
|
||||
// Private
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
|
||||
constructor(
|
||||
private _configService: TreoConfigService,
|
||||
) {
|
||||
// Set the private defaults
|
||||
this._unsubscribeAll = new Subject();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to config changes
|
||||
this._configService.config$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((config: AppConfig) => {
|
||||
|
||||
// Store the config
|
||||
this.dashboardDisplay = config.dashboardDisplay;
|
||||
this.dashboardSort = config.dashboardSort;
|
||||
this.temperatureUnit = config.temperatureUnit;
|
||||
this.theme = config.theme;
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
formatLabel(value: number) {
|
||||
|
||||
saveSettings(): void {
|
||||
|
||||
|
||||
const newSettings = {
|
||||
dashboardDisplay: this.dashboardDisplay,
|
||||
dashboardSort: this.dashboardSort,
|
||||
temperatureUnit: this.temperatureUnit,
|
||||
theme: this.theme
|
||||
}
|
||||
this._configService.config = newSettings
|
||||
console.log(`Saved Settings: ${JSON.stringify(newSettings)}`)
|
||||
}
|
||||
|
||||
formatLabel(value: number): number {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ export class LayoutComponent implements OnInit, OnDestroy
|
||||
|
||||
// Private
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
private systemPrefersDark: boolean;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
@@ -43,6 +44,9 @@ export class LayoutComponent implements OnInit, OnDestroy
|
||||
{
|
||||
// Set the private defaults
|
||||
this._unsubscribeAll = new Subject();
|
||||
|
||||
this.systemPrefersDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
@@ -66,7 +70,7 @@ export class LayoutComponent implements OnInit, OnDestroy
|
||||
this.theme = config.theme;
|
||||
|
||||
// Update the selected theme class name on body
|
||||
const themeName = 'treo-theme-' + config.theme;
|
||||
const themeName = 'treo-theme-' + this.determineTheme(config);
|
||||
this._document.body.classList.forEach((className) => {
|
||||
if ( className.startsWith('treo-theme-') && className !== themeName )
|
||||
{
|
||||
@@ -105,6 +109,17 @@ export class LayoutComponent implements OnInit, OnDestroy
|
||||
// @ Private methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Checks if theme should be set to dark based on config & system settings
|
||||
*/
|
||||
private determineTheme(config:AppConfig): string {
|
||||
if (config.theme === 'system') {
|
||||
return this.systemPrefersDark ? 'dark' : 'light'
|
||||
} else {
|
||||
return config.theme
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the selected layout
|
||||
*/
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
|
||||
<!-- Spacer -->
|
||||
<div class="spacer"></div>
|
||||
|
||||
<code>{{appVersion}}</code>
|
||||
|
||||
<!-- Shortcuts -->
|
||||
<!-- <shortcuts [shortcuts]="data.shortcuts"></shortcuts>-->
|
||||
@@ -48,6 +48,7 @@
|
||||
<!-- <notifications [notifications]="data.notifications"></notifications>-->
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { TreoMediaWatcherService } from '@treo/services/media-watcher';
|
||||
import { TreoNavigationService } from '@treo/components/navigation';
|
||||
import {versionInfo} from 'environments/versions';
|
||||
|
||||
@Component({
|
||||
selector : 'material-layout',
|
||||
@@ -13,6 +14,7 @@ import { TreoNavigationService } from '@treo/components/navigation';
|
||||
})
|
||||
export class MaterialLayoutComponent implements OnInit, OnDestroy
|
||||
{
|
||||
appVersion: string;
|
||||
data: any;
|
||||
isScreenSmall: boolean;
|
||||
|
||||
@@ -46,6 +48,8 @@ export class MaterialLayoutComponent implements OnInit, OnDestroy
|
||||
// Set the defaults
|
||||
this.fixedHeader = false;
|
||||
this.fixedFooter = false;
|
||||
|
||||
this.appVersion = versionInfo.version
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -47,71 +47,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap w-full">
|
||||
<div *ngFor="let summary of data.data.summary | keyvalue" class="flex gt-sm:w-1/2 min-w-80 p-4">
|
||||
<div [ngClass]="{ 'border-green': summary.value.device.device_status == 0 && summary.value.smart,
|
||||
'border-red': summary.value.device.device_status != 0 }"
|
||||
class="relative flex flex-col flex-auto p-6 pr-3 pb-3 bg-card rounded border-l-4 shadow-md overflow-hidden">
|
||||
<div class="absolute bottom-0 right-0 w-24 h-24 -m-6">
|
||||
<mat-icon class="icon-size-96 opacity-12 text-green"
|
||||
*ngIf="summary.value.device.device_status == 0 && summary.value.smart"
|
||||
[svgIcon]="'heroicons_outline:check-circle'"></mat-icon>
|
||||
<mat-icon class="icon-size-96 opacity-12 text-red"
|
||||
*ngIf="summary.value.device.device_status != 0"
|
||||
[svgIcon]="'heroicons_outline:exclamation-circle'"></mat-icon>
|
||||
<mat-icon class="icon-size-96 opacity-12 text-yellow"
|
||||
*ngIf="!summary.value.smart"
|
||||
[svgIcon]="'heroicons_outline:question-mark-circle'"></mat-icon>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="flex flex-col">
|
||||
<a [routerLink]="'/device/'+ summary.value.device.wwn"
|
||||
class="font-bold text-md text-secondary uppercase tracking-wider">{{deviceTitle(summary.value.device)}}</a>
|
||||
<div [ngClass]="classDeviceLastUpdatedOn(summary.value)" class="font-medium text-sm" *ngIf="summary.value.smart">
|
||||
Last Updated on {{summary.value.smart.collector_date | date:'MMMM dd, yyyy - HH:mm' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-auto" *ngIf="summary.value.device">
|
||||
<button mat-icon-button
|
||||
[matMenuTriggerFor]="previousStatementMenu">
|
||||
<mat-icon [svgIcon]="'more_vert'"></mat-icon>
|
||||
</button>
|
||||
<mat-menu #previousStatementMenu="matMenu">
|
||||
<a mat-menu-item [routerLink]="'/device/'+ summary.value.device.wwn">
|
||||
<span class="flex items-center">
|
||||
<mat-icon class="icon-size-20 mr-3"
|
||||
[svgIcon]="'payment'"></mat-icon>
|
||||
<span>View Details</span>
|
||||
</span>
|
||||
</a>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row flex-wrap mt-4 -mx-6">
|
||||
<div class="flex flex-col mx-6 my-3 xs:w-full">
|
||||
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Status</div>
|
||||
<div class="mt-2 font-medium text-3xl leading-none" *ngIf="summary.value.smart?.collector_date; else unknownStatus">{{ deviceStatusString(summary.value.device.device_status) | titlecase}}</div>
|
||||
<ng-template #unknownStatus><div class="mt-2 font-medium text-3xl leading-none">No Data</div></ng-template>
|
||||
</div>
|
||||
<div class="flex flex-col mx-6 my-3 xs:w-full">
|
||||
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Temperature</div>
|
||||
<div class="mt-2 font-medium text-3xl leading-none" *ngIf="summary.value.smart?.collector_date; else unknownTemp">{{ summary.value.smart?.temp }}°C</div>
|
||||
<ng-template #unknownTemp><div class="mt-2 font-medium text-3xl leading-none">--</div></ng-template>
|
||||
</div>
|
||||
<div class="flex flex-col mx-6 my-3 xs:w-full">
|
||||
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Capacity</div>
|
||||
<div class="mt-2 font-medium text-3xl leading-none">{{ summary.value.device.capacity | fileSize}}</div>
|
||||
</div>
|
||||
<div class="flex flex-col mx-6 my-3 xs:w-full">
|
||||
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Powered On</div>
|
||||
<div class="mt-2 font-medium text-3xl leading-none" *ngIf="summary.value.smart?.power_on_hours; else unknownPoweredOn">{{ humanizeDuration(summary.value.smart?.power_on_hours * 60 * 60 * 1000, { round: true, largest: 1, units: ['y', 'd', 'h'] }) }}</div>
|
||||
<ng-template #unknownPoweredOn><div class="mt-2 font-medium text-3xl leading-none">--</div></ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap w-full" *ngFor="let hostId of hostGroups | keyvalue">
|
||||
<h3 class="ml-4" *ngIf="hostId.key">{{hostId.key}}</h3>
|
||||
<div class="flex flex-wrap w-full">
|
||||
<app-dashboard-device (deviceDeleted)="onDeviceDeleted($event)" class="flex gt-sm:w-1/2 min-w-80 p-4" *ngFor="let deviceSummary of (deviceSummariesForHostGroup(hostId.value) | deviceSort:config.dashboardSort:config.dashboardDisplay )" [deviceWWN]="deviceSummary.device.wwn" [deviceSummary]="deviceSummary"></app-dashboard-device>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Drive Temperatures -->
|
||||
<div class="flex flex-auto w-full min-w-80 h-90 p-4">
|
||||
<div class="flex flex-col flex-auto bg-card shadow-md rounded overflow-hidden">
|
||||
@@ -123,22 +67,22 @@
|
||||
</div>
|
||||
<div>
|
||||
<button class="h-8 min-h-8 px-2"
|
||||
matTooltip="not yet implemented"
|
||||
mat-button
|
||||
[matMenuTriggerFor]="tempRangeMenu">
|
||||
<span class="font-medium text-sm text-hint">1 week</span>
|
||||
<span class="font-medium text-sm text-hint">{{tempDurationKey}}</span>
|
||||
</button>
|
||||
<mat-menu #tempRangeMenu="matMenu">
|
||||
<button mat-menu-item>1 month</button>
|
||||
<button mat-menu-item>12 months</button>
|
||||
<button mat-menu-item>all time</button>
|
||||
<button (click)="changeSummaryTempDuration('forever')" mat-menu-item>forever</button>
|
||||
<button (click)="changeSummaryTempDuration('year')" mat-menu-item>year</button>
|
||||
<button (click)="changeSummaryTempDuration('month')" mat-menu-item>month</button>
|
||||
<button (click)="changeSummaryTempDuration('week')" mat-menu-item>week</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="flex flex-col flex-auto">
|
||||
<apx-chart *ngIf="temperatureOptions" class="flex-auto w-full h-full"
|
||||
<apx-chart #tempChart *ngIf="temperatureOptions" class="flex-auto w-full h-full"
|
||||
[chart]="temperatureOptions.chart"
|
||||
[colors]="temperatureOptions.colors"
|
||||
[fill]="temperatureOptions.fill"
|
||||
@@ -167,6 +111,4 @@
|
||||
<code>scrutiny-collector-metrics run</code>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</ng-template>
|
||||
|
||||
@@ -3,12 +3,15 @@ import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { ApexOptions } from 'ng-apexcharts';
|
||||
import {ApexOptions, ChartComponent} from 'ng-apexcharts';
|
||||
import { DashboardService } from 'app/modules/dashboard/dashboard.service';
|
||||
import * as moment from "moment";
|
||||
import {MatDialog} from '@angular/material/dialog';
|
||||
import { DashboardSettingsComponent } from 'app/layout/common/dashboard-settings/dashboard-settings.component';
|
||||
import humanizeDuration from 'humanize-duration'
|
||||
import {AppConfig} from "app/core/config/app.config";
|
||||
import {TreoConfigService} from "@treo/services/config";
|
||||
import {Router} from "@angular/router";
|
||||
import {TemperaturePipe} from "app/shared/temperature.pipe";
|
||||
import {DeviceTitlePipe} from "app/shared/device-title.pipe";
|
||||
|
||||
@Component({
|
||||
selector : 'example',
|
||||
@@ -20,10 +23,14 @@ import humanizeDuration from 'humanize-duration'
|
||||
export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
{
|
||||
data: any;
|
||||
hostGroups: { [hostId: string]: string[] } = {}
|
||||
temperatureOptions: ApexOptions;
|
||||
tempDurationKey: string = "forever"
|
||||
config: AppConfig;
|
||||
|
||||
// Private
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
@ViewChild("tempChart", { static: false }) tempChart: ChartComponent;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
@@ -32,7 +39,9 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
*/
|
||||
constructor(
|
||||
private _smartService: DashboardService,
|
||||
public dialog: MatDialog
|
||||
private _configService: TreoConfigService,
|
||||
public dialog: MatDialog,
|
||||
private router: Router,
|
||||
)
|
||||
{
|
||||
// Set the private defaults
|
||||
@@ -49,6 +58,28 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
|
||||
// Subscribe to config changes
|
||||
this._configService.config$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((config: AppConfig) => {
|
||||
|
||||
//check if the old config and the new config do not match.
|
||||
let oldConfig = JSON.stringify(this.config)
|
||||
let newConfig = JSON.stringify(config)
|
||||
|
||||
if(oldConfig != newConfig){
|
||||
console.log(`Configuration updated: ${newConfig} vs ${oldConfig}`)
|
||||
// Store the config
|
||||
this.config = config;
|
||||
|
||||
if(oldConfig){
|
||||
console.log("reloading component...")
|
||||
this.refreshComponent()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Get the data
|
||||
this._smartService.data$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
@@ -57,6 +88,15 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
// Store the data
|
||||
this.data = data;
|
||||
|
||||
//generate group data.
|
||||
for(let wwn in this.data.data.summary){
|
||||
let hostid = this.data.data.summary[wwn].device.host_id
|
||||
let hostDeviceList = this.hostGroups[hostid] || []
|
||||
hostDeviceList.push(wwn)
|
||||
this.hostGroups[hostid] = hostDeviceList
|
||||
}
|
||||
console.log(this.hostGroups)
|
||||
|
||||
// Prepare the chart data
|
||||
this._prepareChartData();
|
||||
});
|
||||
@@ -81,6 +121,14 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Private methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
private refreshComponent(){
|
||||
|
||||
let currentUrl = this.router.url;
|
||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||
this.router.onSameUrlNavigation = 'reload';
|
||||
this.router.navigate([currentUrl]);
|
||||
}
|
||||
|
||||
private _deviceDataTemperatureSeries() {
|
||||
var deviceTemperatureSeries = []
|
||||
|
||||
@@ -91,8 +139,11 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
if (!deviceSummary.temp_history){
|
||||
continue
|
||||
}
|
||||
|
||||
let deviceName = DeviceTitlePipe.deviceTitleWithFallback(deviceSummary.device, this.config.dashboardDisplay)
|
||||
|
||||
var deviceSeriesMetadata = {
|
||||
name: `/dev/${deviceSummary.device.device_name}`,
|
||||
name: deviceName,
|
||||
data: []
|
||||
}
|
||||
|
||||
@@ -100,7 +151,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
let newDate = new Date(tempHistory.date);
|
||||
deviceSeriesMetadata.data.push({
|
||||
x: newDate,
|
||||
y: tempHistory.temp
|
||||
y: TemperaturePipe.formatTemperature(tempHistory.temp, this.config.temperatureUnit, false)
|
||||
})
|
||||
}
|
||||
deviceTemperatureSeries.push(deviceSeriesMetadata)
|
||||
@@ -149,8 +200,9 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
format: 'MMM dd, yyyy HH:mm:ss'
|
||||
},
|
||||
y : {
|
||||
|
||||
formatter: (value) => {
|
||||
return value + '°C';
|
||||
return TemperaturePipe.formatTemperature(value, this.config.temperatureUnit, true) as string;
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -164,6 +216,16 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
deviceSummariesForHostGroup(hostGroupWWNs: string[]) {
|
||||
let deviceSummaries = []
|
||||
for(let wwn of hostGroupWWNs){
|
||||
if(this.data.data.summary[wwn]){
|
||||
deviceSummaries.push(this.data.data.summary[wwn])
|
||||
}
|
||||
}
|
||||
return deviceSummaries
|
||||
}
|
||||
|
||||
openDialog() {
|
||||
const dialogRef = this.dialog.open(DashboardSettingsComponent);
|
||||
|
||||
@@ -172,48 +234,33 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
});
|
||||
}
|
||||
|
||||
deviceTitle(disk){
|
||||
let title = []
|
||||
|
||||
if (disk.host_id) title.push(disk.host_id)
|
||||
|
||||
title.push(`/dev/${disk.device_name}`)
|
||||
|
||||
if (disk.device_type && disk.device_type != 'scsi' && disk.device_type != 'ata'){
|
||||
title.push(disk.device_type)
|
||||
}
|
||||
|
||||
title.push(disk.model_name)
|
||||
|
||||
return title.join(' - ')
|
||||
onDeviceDeleted(wwn: string) {
|
||||
delete this.data.data.summary[wwn] // remove the device from the summary list.
|
||||
}
|
||||
|
||||
deviceStatusString(deviceStatus){
|
||||
if(deviceStatus == 0){
|
||||
return "passed"
|
||||
} else {
|
||||
return "failed"
|
||||
}
|
||||
}
|
||||
/*
|
||||
|
||||
classDeviceLastUpdatedOn(deviceSummary){
|
||||
if (deviceSummary.device.device_status !== 0) {
|
||||
return 'text-red' // if the device has failed, always highlight in red
|
||||
} else if(deviceSummary.device.device_status === 0 && deviceSummary.smart){
|
||||
if(moment().subtract(14, 'd').isBefore(deviceSummary.smart.collector_date)){
|
||||
// this device was updated in the last 2 weeks.
|
||||
return 'text-green'
|
||||
} else if(moment().subtract(1, 'm').isBefore(deviceSummary.smart.collector_date)){
|
||||
// this device was updated in the last month
|
||||
return 'text-yellow'
|
||||
} else{
|
||||
// last updated more than a month ago.
|
||||
return 'text-red'
|
||||
}
|
||||
DURATION_KEY_WEEK = "week"
|
||||
DURATION_KEY_MONTH = "month"
|
||||
DURATION_KEY_YEAR = "year"
|
||||
DURATION_KEY_FOREVER = "forever"
|
||||
*/
|
||||
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
changeSummaryTempDuration(durationKey: string){
|
||||
this.tempDurationKey = durationKey
|
||||
|
||||
this._smartService.getSummaryTempData(durationKey)
|
||||
.subscribe((data) => {
|
||||
|
||||
// given a list of device temp history, override the data in the "summary" object.
|
||||
for(const wwn in this.data.data.summary) {
|
||||
// console.log(`Updating ${wwn}, length: ${this.data.data.summary[wwn].temp_history.length}`)
|
||||
this.data.data.summary[wwn].temp_history = data.data.temp_history[wwn] || []
|
||||
}
|
||||
|
||||
// Prepare the chart series data
|
||||
this.tempChart.updateSeries(this._deviceDataTemperatureSeries())
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -227,6 +274,4 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
return item.id || index;
|
||||
}
|
||||
|
||||
readonly humanizeDuration = humanizeDuration;
|
||||
|
||||
}
|
||||
|
||||
@@ -13,12 +13,13 @@ import { MatTableModule } from '@angular/material/table';
|
||||
import { NgApexchartsModule } from 'ng-apexcharts';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip'
|
||||
import { DashboardSettingsModule } from "app/layout/common/dashboard-settings/dashboard-settings.module";
|
||||
import { DashboardDeviceModule } from "app/layout/common/dashboard-device/dashboard-device.module";
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
DashboardComponent
|
||||
],
|
||||
imports : [
|
||||
imports: [
|
||||
RouterModule.forChild(dashboardRoutes),
|
||||
MatButtonModule,
|
||||
MatDividerModule,
|
||||
@@ -30,7 +31,8 @@ import { DashboardSettingsModule } from "app/layout/common/dashboard-settings/da
|
||||
MatTableModule,
|
||||
NgApexchartsModule,
|
||||
SharedModule,
|
||||
DashboardSettingsModule
|
||||
DashboardSettingsModule,
|
||||
DashboardDeviceModule
|
||||
]
|
||||
})
|
||||
export class DashboardModule
|
||||
|
||||
@@ -31,6 +31,6 @@ export class DashboardResolver implements Resolve<any>
|
||||
*/
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any>
|
||||
{
|
||||
return this._dashboardService.getData();
|
||||
return this._dashboardService.getSummaryData();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export class DashboardService
|
||||
/**
|
||||
* Get data
|
||||
*/
|
||||
getData(): Observable<any>
|
||||
getSummaryData(): Observable<any>
|
||||
{
|
||||
return this._httpClient.get(getBasePath() + '/api/summary').pipe(
|
||||
tap((response: any) => {
|
||||
@@ -52,4 +52,14 @@ export class DashboardService
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getSummaryTempData(durationKey: string): Observable<any>
|
||||
{
|
||||
let params = {}
|
||||
if(durationKey){
|
||||
params["duration_key"] = durationKey
|
||||
}
|
||||
|
||||
return this._httpClient.get(getBasePath() + '/api/summary/temp', {params: params});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<div class="flex items-center justify-between w-full my-4 px-4 xs:pr-0">
|
||||
<div class="mr-6">
|
||||
<h2 class="m-0">Drive Details</h2>
|
||||
<h2 class="m-0">Drive Details - {{device | deviceTitle:config.dashboardDisplay}} </h2>
|
||||
<div class="text-secondary tracking-tight">Dive into S.M.A.R.T data</div>
|
||||
</div>
|
||||
<!-- Action buttons -->
|
||||
@@ -51,11 +51,8 @@
|
||||
|
||||
<!-- Card -->
|
||||
<div class="flex flex-auto w-1/4 p-4 lt-md:w-full">
|
||||
<treo-card class="flex flex-auto p-4 pt-6 flex-col flex-auto filter-list">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-2xl font-semibold leading-tight">/dev/{{device?.device_name}}</div>
|
||||
</div>
|
||||
<div class="flex flex-col my-2 grid grid-cols-2">
|
||||
<treo-card class="flex flex-auto p-4 flex-col flex-auto filter-list">
|
||||
<div class="flex flex-col grid grid-cols-2">
|
||||
<div *ngIf="device" class="my-2 col-span-2 lt-md:col-span-1">
|
||||
<div>
|
||||
<span class="inline-flex items-center font-bold text-xs px-2 py-2px rounded-full tracking-wide uppercase"
|
||||
@@ -64,7 +61,7 @@
|
||||
<span class="w-2 h-2 rounded-full mr-2"
|
||||
[ngClass]="{'bg-red': device?.device_status != 0,
|
||||
'bg-green': device?.device_status == 0}"></span>
|
||||
<span class="pr-2px leading-relaxed whitespace-no-wrap">{{device?.device_status == 0 ? 'passed' : 'failed'}}</span>
|
||||
<span class="pr-2px leading-relaxed whitespace-no-wrap">{{device?.device_status | deviceStatus}}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-secondary text-md">Status</div>
|
||||
@@ -74,6 +71,16 @@
|
||||
<div>{{device?.host_id}}</div>
|
||||
<div class="text-secondary text-md">Host ID</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="device?.device_uuid" class="my-2 col-span-2 lt-md:col-span-1">
|
||||
<div>{{device?.device_uuid}}</div>
|
||||
<div class="text-secondary text-md">Device UUID</div>
|
||||
</div>
|
||||
<div *ngIf="device?.device_label" class="my-2 col-span-2 lt-md:col-span-1">
|
||||
<div>{{device?.device_label}}</div>
|
||||
<div class="text-secondary text-md">Device Label</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="device?.device_type && device?.device_type != 'ata' && device?.device_type != 'scsi'" class="my-2 col-span-2 lt-md:col-span-1">
|
||||
<div>{{device?.device_type | uppercase}}</div>
|
||||
<div class="text-secondary text-md">Device Type</div>
|
||||
@@ -119,7 +126,7 @@
|
||||
<div class="text-secondary text-md">Powered On</div>
|
||||
</div>
|
||||
<div class="my-2 col-span-2 lt-md:col-span-1">
|
||||
<div>{{smart_results[0]?.temp}}°C</div>
|
||||
<div>{{smart_results[0]?.temp | temperature:config.temperatureUnit:true}}</div>
|
||||
<div class="text-secondary text-md">Temperature</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,8 @@ import {fadeOut} from "../../../@treo/animations/fade";
|
||||
import {DetailSettingsComponent} from "app/layout/common/detail-settings/detail-settings.component";
|
||||
import {MatDialog} from "@angular/material/dialog";
|
||||
import humanizeDuration from 'humanize-duration';
|
||||
import {TreoConfigService} from "../../../@treo/services/config";
|
||||
import {AppConfig} from "../../core/config/app.config";
|
||||
|
||||
@Component({
|
||||
selector: 'detail',
|
||||
@@ -18,6 +20,8 @@ import humanizeDuration from 'humanize-duration';
|
||||
|
||||
export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
config: AppConfig;
|
||||
|
||||
onlyCritical: boolean = true;
|
||||
// data: any;
|
||||
|
||||
@@ -43,7 +47,9 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
*/
|
||||
constructor(
|
||||
private _detailService: DetailService,
|
||||
public dialog: MatDialog
|
||||
public dialog: MatDialog,
|
||||
private _configService: TreoConfigService,
|
||||
|
||||
|
||||
)
|
||||
{
|
||||
@@ -65,6 +71,14 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
// Subscribe to config changes
|
||||
this._configService.config$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((config: AppConfig) => {
|
||||
|
||||
this.config = config;
|
||||
});
|
||||
|
||||
// Get the data
|
||||
this._detailService.data$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
@@ -107,25 +121,34 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Private methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
getAttributeStatusName(attribute_status){
|
||||
if(attribute_status == 0){
|
||||
return "passed"
|
||||
} else if (attribute_status == 1){
|
||||
return "failed"
|
||||
} else if (attribute_status == 2){
|
||||
return "warn"
|
||||
getAttributeStatusName(attributeStatus: number): string {
|
||||
// tslint:disable:no-bitwise
|
||||
|
||||
// from Constants.go
|
||||
// AttributeStatusPassed AttributeStatus = 0
|
||||
// AttributeStatusFailedSmart AttributeStatus = 1
|
||||
// AttributeStatusWarningScrutiny AttributeStatus = 2
|
||||
// AttributeStatusFailedScrutiny AttributeStatus = 4
|
||||
|
||||
if(attributeStatus === 0){
|
||||
return 'passed'
|
||||
|
||||
} else if ((attributeStatus & 1) !== 0 || (attributeStatus & 4) !== 0 ){
|
||||
return 'failed'
|
||||
} else if ((attributeStatus & 2) !== 0){
|
||||
return 'warn'
|
||||
}
|
||||
return
|
||||
return ''
|
||||
// tslint:enable:no-bitwise
|
||||
}
|
||||
|
||||
getAttributeName(attribute_data){
|
||||
getAttributeName(attribute_data): string {
|
||||
let attribute_metadata = this.metadata[attribute_data.attribute_id]
|
||||
if(!attribute_metadata){
|
||||
return 'Unknown Attribute Name'
|
||||
} else {
|
||||
return attribute_metadata.display_name
|
||||
}
|
||||
return
|
||||
}
|
||||
getAttributeDescription(attribute_data){
|
||||
let attribute_metadata = this.metadata[attribute_data.attribute_id]
|
||||
|
||||
@@ -1,33 +1,70 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import {DeviceTitlePipe} from "./device-title.pipe";
|
||||
|
||||
@Pipe({
|
||||
name: 'deviceSort'
|
||||
})
|
||||
export class DeviceSortPipe implements PipeTransform {
|
||||
|
||||
numericalStatus(device): number {
|
||||
if(!device.smart_results[0]){
|
||||
return 0
|
||||
} else if (device.smart_results[0].smart_status == 'passed'){
|
||||
return 1
|
||||
} else {
|
||||
return -1
|
||||
statusCompareFn(a: any, b: any) {
|
||||
function deviceStatus(deviceSummary): number {
|
||||
if(!deviceSummary.smart){
|
||||
return 0
|
||||
} else if (deviceSummary.device.device_status == 0){
|
||||
return 1
|
||||
} else {
|
||||
return deviceSummary.device.device_status * -1 // will return range from -1, -2, -3
|
||||
}
|
||||
}
|
||||
|
||||
let left = deviceStatus(a)
|
||||
let right = deviceStatus(b)
|
||||
|
||||
return left - right;
|
||||
}
|
||||
|
||||
titleCompareFn(dashboardDisplay: string) {
|
||||
return function (a: any, b: any){
|
||||
let _dashboardDisplay = dashboardDisplay
|
||||
let left = DeviceTitlePipe.deviceTitleForType(a.device, _dashboardDisplay) || DeviceTitlePipe.deviceTitleForType(a.device, 'name')
|
||||
let right = DeviceTitlePipe.deviceTitleForType(b.device, _dashboardDisplay) || DeviceTitlePipe.deviceTitleForType(b.device, 'name')
|
||||
|
||||
if( left < right )
|
||||
return -1;
|
||||
|
||||
if( left > right )
|
||||
return 1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
ageCompareFn(a: any, b: any) {
|
||||
const left = a.smart?.power_on_hours
|
||||
const right = b.smart?.power_on_hours
|
||||
|
||||
transform(devices: Array<unknown>, ...args: unknown[]): Array<unknown> {
|
||||
//failed, unknown/empty, passed
|
||||
devices.sort((a: any, b: any) => {
|
||||
|
||||
let left = this.numericalStatus(a)
|
||||
let right = this.numericalStatus(b)
|
||||
|
||||
return left - right;
|
||||
});
|
||||
return left - right;
|
||||
}
|
||||
|
||||
|
||||
return devices;
|
||||
transform(deviceSummaries: Array<unknown>, sortBy = 'status', dashboardDisplay = 'name'): Array<unknown> {
|
||||
let compareFn: any
|
||||
switch (sortBy) {
|
||||
case 'status':
|
||||
compareFn = this.statusCompareFn
|
||||
break;
|
||||
case 'title':
|
||||
compareFn = this.titleCompareFn(dashboardDisplay)
|
||||
break;
|
||||
case 'age':
|
||||
compareFn = this.ageCompareFn
|
||||
break;
|
||||
}
|
||||
|
||||
// failed, unknown/empty, passed
|
||||
deviceSummaries.sort(compareFn);
|
||||
|
||||
return deviceSummaries;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { DeviceStatusPipe } from './device-status.pipe';
|
||||
|
||||
describe('DeviceStatusPipe', () => {
|
||||
it('create an instance', () => {
|
||||
const pipe = new DeviceStatusPipe();
|
||||
expect(pipe).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
@Pipe({
|
||||
name: 'deviceStatus'
|
||||
})
|
||||
export class DeviceStatusPipe implements PipeTransform {
|
||||
|
||||
transform(deviceStatusFlag: number): string {
|
||||
if(deviceStatusFlag === 0){
|
||||
return 'passed'
|
||||
} else if(deviceStatusFlag === 3){
|
||||
return 'failed: both'
|
||||
} else if(deviceStatusFlag === 2) {
|
||||
return 'failed: scrutiny'
|
||||
} else if(deviceStatusFlag === 1) {
|
||||
return 'failed: smart'
|
||||
}
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { DeviceTitlePipe } from './device-title.pipe';
|
||||
|
||||
describe('DeviceTitlePipe', () => {
|
||||
it('create an instance', () => {
|
||||
const pipe = new DeviceTitlePipe();
|
||||
expect(pipe).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
@Pipe({
|
||||
name: 'deviceTitle'
|
||||
})
|
||||
export class DeviceTitlePipe implements PipeTransform {
|
||||
|
||||
static deviceTitleForType(device: any, titleType: string): string {
|
||||
const titleParts = []
|
||||
switch(titleType){
|
||||
case 'name':
|
||||
titleParts.push(`/dev/${device.device_name}`)
|
||||
if (device.device_type && device.device_type !== 'scsi' && device.device_type !== 'ata'){
|
||||
titleParts.push(device.device_type)
|
||||
}
|
||||
titleParts.push(device.model_name)
|
||||
|
||||
break;
|
||||
case 'serial_id':
|
||||
if(!device.device_serial_id) return ''
|
||||
titleParts.push(`/by-id/${device.device_serial_id}`)
|
||||
break;
|
||||
case 'uuid':
|
||||
if(!device.device_uuid) return ''
|
||||
titleParts.push(`/by-uuid/${device.device_uuid}`)
|
||||
break;
|
||||
case 'label':
|
||||
if(device.label){
|
||||
titleParts.push(device.label)
|
||||
} else if(device.device_label){
|
||||
titleParts.push(`/by-label/${device.device_label}`)
|
||||
}
|
||||
break;
|
||||
}
|
||||
return titleParts.join(' - ')
|
||||
}
|
||||
|
||||
static deviceTitleWithFallback(device, titleType: string): string {
|
||||
console.log(`Displaying Device ${device.wwn} with: ${titleType}`)
|
||||
const titleParts = []
|
||||
if (device.host_id) titleParts.push(device.host_id)
|
||||
|
||||
// add device identifier (fallback to generated device name)
|
||||
titleParts.push(DeviceTitlePipe.deviceTitleForType(device, titleType) || DeviceTitlePipe.deviceTitleForType(device, 'name'))
|
||||
|
||||
return titleParts.join(' - ')
|
||||
}
|
||||
|
||||
|
||||
transform(device: any, titleType: string = 'name'): string {
|
||||
return DeviceTitlePipe.deviceTitleWithFallback(device, titleType)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +1,19 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import {FileSizePipe} from "./file-size.pipe";
|
||||
import {FileSizePipe} from './file-size.pipe';
|
||||
import { DeviceSortPipe } from './device-sort.pipe';
|
||||
import { TemperaturePipe } from './temperature.pipe';
|
||||
import { DeviceTitlePipe } from './device-title.pipe';
|
||||
import { DeviceStatusPipe } from './device-status.pipe';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
FileSizePipe,
|
||||
DeviceSortPipe
|
||||
DeviceSortPipe,
|
||||
TemperaturePipe,
|
||||
DeviceTitlePipe,
|
||||
DeviceStatusPipe
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@@ -19,7 +25,10 @@ import { DeviceSortPipe } from './device-sort.pipe';
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
FileSizePipe,
|
||||
DeviceSortPipe
|
||||
DeviceSortPipe,
|
||||
DeviceTitlePipe,
|
||||
DeviceStatusPipe,
|
||||
TemperaturePipe
|
||||
]
|
||||
})
|
||||
export class SharedModule
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { TemperaturePipe } from './temperature.pipe';
|
||||
|
||||
describe('TemperaturePipe', () => {
|
||||
it('create an instance', () => {
|
||||
const pipe = new TemperaturePipe();
|
||||
expect(pipe).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import {formatNumber} from "@angular/common";
|
||||
|
||||
@Pipe({
|
||||
name: 'temperature'
|
||||
})
|
||||
export class TemperaturePipe implements PipeTransform {
|
||||
static celsiusToFahrenheit(celsiusTemp: number): number {
|
||||
return celsiusTemp * 9.0 / 5.0 + 32;
|
||||
}
|
||||
static formatTemperature(celsiusTemp: number, unit: string, includeUnits: boolean): number|string {
|
||||
let convertedTemp
|
||||
let convertedUnitSuffix
|
||||
switch (unit) {
|
||||
case 'celsius':
|
||||
convertedTemp = celsiusTemp
|
||||
convertedUnitSuffix = '°C'
|
||||
break
|
||||
case 'fahrenheit':
|
||||
convertedTemp = TemperaturePipe.celsiusToFahrenheit(celsiusTemp)
|
||||
convertedUnitSuffix = '°F'
|
||||
break
|
||||
}
|
||||
if(includeUnits){
|
||||
return formatNumber(convertedTemp, 'en-US') + convertedUnitSuffix
|
||||
} else {
|
||||
return formatNumber(convertedTemp, 'en-US',)
|
||||
}
|
||||
}
|
||||
|
||||
transform(celsiusTemp: number, unit = 'celsius', includeUnits = false): number|string {
|
||||
return TemperaturePipe.formatTemperature(celsiusTemp, unit, includeUnits)
|
||||
}
|
||||
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,329 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<defs >
|
||||
<font id="IBMPlexMono" horiz-adv-x="600" ><font-face
|
||||
font-family="IBM Plex Mono Medium"
|
||||
units-per-em="1000"
|
||||
panose-1="2 11 6 9 5 2 3 0 2 3"
|
||||
ascent="1025"
|
||||
descent="-275"
|
||||
alphabetic="0" />
|
||||
<glyph unicode=" " glyph-name="space" />
|
||||
<glyph unicode="!" glyph-name="exclam" d="M281 237L240 495V698H360V495L319 237H281ZM300 -10Q254 -10 235 9T216 57V81Q216 110 235 129T300 148Q346 148 365 129T384 81V57Q384 28 365 9T300 -10Z" />
|
||||
<glyph unicode=""" glyph-name="quotedbl" d="M408 442V740H508V442H408ZM349 442V740H449V442H349Z" />
|
||||
<glyph unicode="#" glyph-name="numbersign" d="M135 207H13V291H150L170 407H50V491H185L222 698H313L190 0H99L135 207ZM410 698H501L465 491H587V407H450L430 291H550V207H415L378 0H287L410 698Z" />
|
||||
<glyph unicode="$" glyph-name="dollar" d="M268 -11Q188 -5 133 26T43 105L115 174Q149 133 187 111T273 83V312L250 316Q200 325 165 343T108 387T76 444T66 512Q66 597 119 647T268 708V811H352V708Q418 701 466 675T549 604L476 538Q452 569 421 588T347 612V400L374
|
||||
396Q424 387 459 369T516 326T549 268T559 200Q559 114 506 59T352 -8V-113H268V-11ZM172 515Q172 473 195 450T273 414V614Q172 602 172 515ZM453 195Q453 240 429 263T347 298V84Q399 92 426 120T453 195Z" />
|
||||
<glyph unicode="%" glyph-name="percent" d="M167 348Q98 348 57 394T15 529Q15 617 56 663T167 710Q236 710 277 664T319 529Q319 441 278 395T167 348ZM167 416Q200 416 218 439T236 505V553Q236 595 218 618T167 642Q134 642 116 619T98 553V505Q98 463 116
|
||||
440T167 416ZM499 698H589L432 398H343L499 698ZM168 300H257L101 0H11L168 300ZM433 -12Q364 -12 323 34T281 169Q281 257 322 303T433 350Q502 350 543 304T585 169Q585 81 544 35T433 -12ZM433 56Q466 56 484 79T502 145V193Q502 235 484 258T433 282Q400 282
|
||||
382 259T364 193V145Q364 103 382 80T433 56Z" />
|
||||
<glyph unicode="&" glyph-name="ampersand" d="M213 -12Q167 -12 131 3T71 46T33 108T20 185Q20 242 48 294T145 383Q115 422 98 459T80 538Q80 576 94 607T134 662T193 697T267 710Q302 710 331 700T382 674T420 638T443 596L362 554Q350 585 325 604T265
|
||||
624Q227 624 204 601T180 540V533Q180 518 184 504T197 474T220 439T256 392L332 298L395 215H400Q407 252 409 300T412 390H569V305H496Q490 263 483 228T458 150L583 0H462L378 100H372Q361 49 320 19T213 -12ZM241 77Q275 77 302 90T347 133L192 324Q154 297
|
||||
140 265T126 195V186Q126 135 157 106T241 77Z" />
|
||||
<glyph unicode="'" glyph-name="quotesingle" d="M250 442V740H350V442H250Z" />
|
||||
<glyph unicode="(" glyph-name="parenleft" d="M192 311Q192 384 208 451T253 577T320 681T400 760H509Q464 728 424 685T354 590T306 480T288 357V265Q288 201 306 143T354 32T424 -63T509 -138H400Q358 -107 320 -60T254 45T209 170T192 311Z" />
|
||||
<glyph unicode=")" glyph-name="parenright" d="M408 311Q408 238 392 171T347 45T280 -60T200 -138H91Q136 -106 176 -63T246 32T294 142T312 265V357Q312 421 294 479T246 590T176 685T91 760H200Q242 728 280 682T346 577T391 452T408 311Z" />
|
||||
<glyph unicode="*" glyph-name="asterisk" d="M182 48L105 101L225 263L38 326L66 412L252 349V549H348V349L534 412L562 326L375 263L495 101L418 48L300 210L182 48Z" />
|
||||
<glyph unicode="+" glyph-name="plus" d="M251 62V261H62V350H251V549H349V350H538V261H349V62H251Z" />
|
||||
<glyph unicode="," glyph-name="comma" d="M244 152H399L275 -145H190L244 152Z" />
|
||||
<glyph unicode="-" glyph-name="hyphen" d="M149 250V359H451V250H149Z" />
|
||||
<glyph unicode="." glyph-name="period" d="M300 -10Q252 -10 233 10T213 59V85Q213 114 232 134T300 154Q348 154 367 134T387 85V59Q387 30 368 10T300 -10Z" />
|
||||
<glyph unicode="/" glyph-name="slash" d="M83 -138L420 760H517L180 -138H83Z" />
|
||||
<glyph unicode="0" glyph-name="zero" d="M300 -12Q234 -12 186 12T106 83T59 196T44 349Q44 434 59 501T105 615T185 685T300 710Q366 710 414 686T494 615T541 502T556 349Q556 263 541 196T495 83T415 13T300 -12ZM300 80Q376 80 408 136T441 289V409Q441 505
|
||||
409 561T300 618Q262 618 235 604T191 562T167 496T159 409V289Q159 241 166 203T191 137T235 95T300 80ZM300 290Q265 290 252 304T238 338V360Q238 380 251 394T300 408Q335 408 348 394T362 360V338Q362 318 349 304T300 290Z" />
|
||||
<glyph unicode="1" glyph-name="one" d="M88 0V93H284V616H276L114 443L45 505L223 698H396V93H565V0H88Z" />
|
||||
<glyph unicode="2" glyph-name="two" d="M545 0H63V107L291 306Q345 354 374 397T404 488V499Q404 554 374 583T287 613Q229 613 198 582T152 503L54 540Q64 573 83 603T131 658T201 696T295 710Q349 710 391 695T463 652T507 588T522 505Q522 463 510 428T475
|
||||
360T421 297T353 237L184 95H545V0Z" />
|
||||
<glyph unicode="3" glyph-name="three" d="M271 410Q338 410 370 438T403 510V517Q403 565 372 590T287 616Q236 616 201 593T141 530L65 595Q81 618 102 639T150 675T212 700T291 710Q340 710 382 698T455 663T503 606T520 530Q520 497 510 470T481 423T437 389T384
|
||||
370V365Q414 359 441 346T489 311T522 260T534 192Q534 146 516 109T465 44T384 3T279 -12Q230 -12 193 -2T126 26T76 66T38 113L121 177Q147 135 182 109T280 82Q346 82 381 112T416 197V205Q416 259 380 287T274 316H199V410H271Z" />
|
||||
<glyph unicode="4" glyph-name="four" d="M362 0V136H30V232L322 698H469V224H567V136H469V0H362ZM125 224H362V595H356L125 224Z" />
|
||||
<glyph unicode="5" glyph-name="five" d="M511 601H194L176 353H184Q208 394 242 418T335 443Q380 443 418 428T485 385T530 317T547 225Q547 173 530 130T481 55T402 6T295 -12Q248 -12 212 -2T147 26T97 66T60 113L142 177Q155 157 169 140T202 109T243 89T297
|
||||
82Q362 82 396 117T430 214V222Q430 283 396 317T297 351Q250 351 223 334T176 296L83 309L109 698H511V601Z" />
|
||||
<glyph unicode="6" glyph-name="six" d="M302 -12Q243 -12 197 7T119 61T70 147T53 260Q53 336 76 403T136 526T218 625T307 698H457Q397 655 349 615T266 533T206 445T169 341L176 339Q187 360 202 378T237 411T283 433T342 441Q387 441 425 426T491 383T534
|
||||
314T550 224Q550 172 533 129T483 54T404 6T302 -12ZM301 78Q365 78 400 112T435 210V220Q435 283 400 317T301 352Q238 352 203 318T168 220V210Q168 147 203 113T301 78Z" />
|
||||
<glyph unicode="7" glyph-name="seven" d="M169 0L428 605H156V476H59V698H540V600L288 0H169Z" />
|
||||
<glyph unicode="8" glyph-name="eight" d="M300 -12Q238 -12 191 3T112 46T64 110T47 191Q47 261 86 303T188 362V370Q134 389 101 430T68 531Q68 612 128 661T300 710Q411 710 471 661T532 531Q532 471 499 430T412 370V362Q475 345 514 303T553 191Q553 147
|
||||
537 110T488 46T409 4T300 -12ZM300 77Q366 77 401 106T437 188V209Q437 262 402 291T300 320Q234 320 199 291T163 209V188Q163 135 198 106T300 77ZM300 406Q360 406 391 432T423 507V520Q423 568 392 594T300 621Q240 621 209 595T177 520V507Q177 459 208 433T300
|
||||
406Z" />
|
||||
<glyph unicode="9" glyph-name="nine" d="M547 438Q547 362 524 295T464 172T382 72T293 0H143Q203 43 251 83T334 165T394 253T431 357L424 359Q413 338 398 320T363 287T317 265T258 257Q213 257 175 272T109 315T66 384T50 474Q50 526 67 569T117 644T196 692T298
|
||||
710Q356 710 402 691T481 637T530 551T547 438ZM299 346Q362 346 397 380T432 478V488Q432 551 397 585T299 620Q235 620 200 586T165 488V478Q165 415 200 381T299 346Z" />
|
||||
<glyph unicode=":" glyph-name="colon" d="M300 -10Q252 -10 233 10T213 59V85Q213 114 232 134T300 154Q348 154 367 134T387 85V59Q387 30 368 10T300 -10ZM300 362Q252 362 233 382T213 431V457Q213 486 232 506T300 526Q348 526 367 506T387 457V431Q387 402
|
||||
368 382T300 362Z" />
|
||||
<glyph unicode=";" glyph-name="semicolon" d="M244 152H399L275 -145H190L244 152ZM300 362Q252 362 233 382T213 431V457Q213 486 232 506T300 526Q348 526 367 506T387 457V431Q387 402 368 382T300 362Z" />
|
||||
<glyph unicode="<" glyph-name="less" d="M85 253V357L515 598V492L184 309V301L515 119V12L85 253Z" />
|
||||
<glyph unicode="=" glyph-name="equal" d="M62 367V456H538V367H62ZM62 155V244H538V155H62Z" />
|
||||
<glyph unicode=">" glyph-name="greater" d="M85 118L416 301V309L85 491V598L515 357V253L85 12V118Z" />
|
||||
<glyph unicode="?" glyph-name="question" d="M226 223V378Q307 381 354 410T401 503V517Q401 567 372 591T293 615Q241 615 209 586T165 511L70 549Q79 580 97 609T143 661T209 696T297 710Q347 710 387 696T457 657T501 595T517 514Q517 469 501 434T459 374T400
|
||||
333T331 311V223H226ZM281 -10Q235 -10 216 9T197 57V81Q197 110 216 129T281 148Q327 148 346 129T365 81V57Q365 28 346 9T281 -10Z" />
|
||||
<glyph unicode="@" glyph-name="at" d="M451 -112H335Q256 -112 202 -87T115 -10T67 120T52 302Q52 417 68 495T116 621T194 689T301 710Q363 710 407 691T481 639T523 561T537 463V91H451V150H445Q434 119 411 100T347 80Q288 80 253 131T218 294Q218 405 253
|
||||
456T347 508Q388 508 411 488T445 438H451V463Q451 633 305 633Q264 633 233 619T182 571T152 486T142 358V229Q142 171 151 123T182 39T241 -15T335 -35H451V-112ZM381 150Q413 150 432 168T451 220V368Q451 402 432 420T381 438Q346 438 327 414T308 326V262Q308
|
||||
199 327 175T381 150Z" />
|
||||
<glyph unicode="A" glyph-name="A" d="M462 0L409 186H187L134 0H20L224 698H377L581 0H462ZM302 585H294L208 279H388L302 585Z" />
|
||||
<glyph unicode="B" glyph-name="B" d="M83 698H324Q419 698 471 649T524 515Q524 454 494 418T413 367V362Q474 348 511 309T548 197Q548 154 534 118T494 56T432 15T351 0H83V698ZM317 86Q373 86 403 109T433 183V219Q433 270 403 293T317 316H193V86H317ZM301
|
||||
398Q354 398 381 418T409 487V523Q409 571 382 591T301 612H193V398H301Z" />
|
||||
<glyph unicode="C" glyph-name="C" d="M320 -12Q187 -12 123 82T58 349Q58 522 122 616T320 710Q371 710 408 697T473 660T518 605T549 537L448 503Q440 527 430 548T405 584T370 608T319 617Q245 617 211 561T177 408V290Q177 194 211 138T319 81Q349 81 369
|
||||
90T405 114T430 150T448 195L549 161Q537 125 519 94T473 39T409 2T320 -12Z" />
|
||||
<glyph unicode="D" glyph-name="D" d="M82 698H290Q424 698 488 609T553 349Q553 178 489 89T290 0H82V698ZM283 92Q360 92 397 143T435 291V407Q435 503 398 554T283 606H193V92H283Z" />
|
||||
<glyph unicode="E" glyph-name="E" d="M86 0V698H523V604H198V401H511V307H198V94H523V0H86Z" />
|
||||
<glyph unicode="F" glyph-name="F" d="M86 0V698H535V604H198V401H504V307H198V0H86Z" />
|
||||
<glyph unicode="G" glyph-name="G" d="M430 96H423Q414 75 402 56T371 21T326 -3T266 -12Q157 -12 102 81T47 344Q47 522 110 616T303 710Q354 710 392 696T458 659T503 604T532 537L431 503Q423 526 413 546T389 583T354 608T304 617Q231 617 199 561T166 408V297Q166
|
||||
249 173 209T196 141T239 96T304 80Q365 80 397 116T430 210V273H295V357H532V0H430V96Z" />
|
||||
<glyph unicode="H" glyph-name="H" d="M416 307H184V0H72V698H184V401H416V698H528V0H416V307Z" />
|
||||
<glyph unicode="I" glyph-name="I" d="M80 0V85H244V613H80V698H520V613H356V85H520V0H80Z" />
|
||||
<glyph unicode="J" glyph-name="J" d="M493 698V187Q493 142 478 105T434 42T365 2T276 -12Q184 -12 130 34T59 161L165 183Q174 140 199 111T277 82Q326 82 353 111T381 202V604H138V698H493Z" />
|
||||
<glyph unicode="K" glyph-name="K" d="M274 321L190 216V0H78V698H190V364H195L280 480L451 698H580L350 404L589 0H461L274 321Z" />
|
||||
<glyph unicode="L" glyph-name="L" d="M108 0V698H220V94H538V0H108Z" />
|
||||
<glyph unicode="M" glyph-name="M" d="M445 334L448 544H440L300 185L160 544H152L155 334V0H56V698H193L300 421H307L415 698H544V0H445V334Z" />
|
||||
<glyph unicode="N" glyph-name="N" d="M179 537H170V0H72V698H216L421 161H430V698H528V0H384L179 537Z" />
|
||||
<glyph unicode="O" glyph-name="O" d="M300 -12Q234 -12 186 12T106 83T59 196T44 349Q44 434 59 501T105 615T185 685T300 710Q366 710 414 686T494 615T541 502T556 349Q556 263 541 196T495 83T415 13T300 -12ZM300 81Q375 81 406 137T437 291V408Q437 504
|
||||
406 560T300 617Q225 617 194 561T163 408V290Q163 194 194 138T300 81Z" />
|
||||
<glyph unicode="P" glyph-name="P" d="M84 0V698H345Q445 698 497 642T550 488Q550 390 498 334T345 278H196V0H84ZM196 371H328Q432 371 432 467V510Q432 605 328 605H196V371Z" />
|
||||
<glyph unicode="Q" glyph-name="Q" d="M506 -175H378Q310 -175 280 -140T250 -46V-8Q197 1 158 29T94 102T56 210T44 349Q44 434 59 501T105 615T185 685T300 710Q366 710 414 686T494 615T541 502T556 349Q556 195 506 102T350 -8V-91H506V-175ZM300 81Q375 81
|
||||
406 137T437 291V408Q437 504 406 560T300 617Q225 617 194 561T163 408V290Q163 194 194 138T300 81Z" />
|
||||
<glyph unicode="R" glyph-name="R" d="M195 0H84V698H345Q445 698 497 642T550 488Q550 408 511 356T399 292L560 0H436L288 282H195V0ZM329 372Q433 372 433 467V510Q433 605 329 605H195V372H329Z" />
|
||||
<glyph unicode="S" glyph-name="S" d="M294 -12Q203 -12 141 20T39 105L111 174Q151 126 196 104T298 81Q364 81 399 111T435 197Q435 242 409 266T320 302L244 314Q194 322 160 340T105 384T75 441T65 508Q65 607 129 658T305 710Q388 710 446 684T541 609L471
|
||||
539Q442 574 403 595T305 617Q243 617 211 591T178 513Q178 470 203 446T294 410L368 397Q462 380 505 329T548 203Q548 155 532 115T483 47T403 4T294 -12Z" />
|
||||
<glyph unicode="T" glyph-name="T" d="M356 604V0H244V604H25V698H575V604H356Z" />
|
||||
<glyph unicode="U" glyph-name="U" d="M181 698V269Q181 226 183 191T198 132T234 94T300 81Q342 81 366 94T402 131T416 191T419 269V698H531V289Q531 214 523 158T489 64T419 7T300 -12Q227 -12 182 7T111 63T78 157T69 289V698H181Z" />
|
||||
<glyph unicode="V" glyph-name="V" d="M222 0L25 698H146L238 346L299 109H305L366 346L459 698H575L378 0H222Z" />
|
||||
<glyph unicode="W" glyph-name="W" d="M75 0L25 698H123L148 292L159 113H167L246 529H357L436 113H444L455 292L481 698H575L525 0H375L304 404H296L225 0H75Z" />
|
||||
<glyph unicode="X" glyph-name="X" d="M582 0H455L376 138L302 270H296L220 138L138 0H19L233 358L32 698H159L230 573L302 444H307L379 573L453 698H572L370 358L582 0Z" />
|
||||
<glyph unicode="Y" glyph-name="Y" d="M244 0V263L12 698H137L226 524L298 375H304L377 524L466 698H588L356 263V0H244Z" />
|
||||
<glyph unicode="Z" glyph-name="Z" d="M552 0H48V99L415 604H62V698H538V599L171 94H552V0Z" />
|
||||
<glyph unicode="[" glyph-name="bracketleft" d="M207 -138V760H506V682H297V-60H506V-138H207Z" />
|
||||
<glyph unicode="\" glyph-name="backslash" d="M420 -138L83 760H180L517 -138H420Z" />
|
||||
<glyph unicode="]" glyph-name="bracketright" d="M393 760V-138H94V-60H303V682H94V760H393Z" />
|
||||
<glyph unicode="^" glyph-name="asciicircum" d="M475 267L301 601H294L120 267L37 307L241 698H359L563 307L475 267Z" />
|
||||
<glyph unicode="_" glyph-name="underscore" d="M60 -179V-85H540V-179H60Z" />
|
||||
<glyph unicode="`" glyph-name="grave" d="M171 745L265 791L356 611L289 579L171 745Z" />
|
||||
<glyph unicode="a" glyph-name="a" d="M492 0Q445 0 422 24T393 87H388Q373 40 334 14T233 -12Q154 -12 107 29T60 144Q60 219 115 258T286 298H385V338Q385 443 274 443Q224 443 193 424T141 372L76 425Q98 467 150 497T284 528Q382 528 438 481T494 345V86H559V0H492ZM264
|
||||
68Q317 68 351 92T385 154V229H288Q170 229 170 158V138Q170 103 195 86T264 68Z" />
|
||||
<glyph unicode="b" glyph-name="b" d="M79 740H188V428H194Q217 475 255 501T350 528Q441 528 495 460T549 258Q549 124 495 56T350 -12Q293 -12 255 14T194 88H188V0H79V740ZM305 77Q367 77 400 115T434 216V300Q434 363 401 401T305 439Q281 439 260 433T223
|
||||
415T198 386T188 344V172Q188 148 197 131T223 101T260 83T305 77Z" />
|
||||
<glyph unicode="c" glyph-name="c" d="M319 -12Q261 -12 216 7T139 61T91 146T74 258Q74 320 90 370T138 455T215 509T318 528Q398 528 446 494T519 406L434 360Q420 396 392 417T318 438Q256 438 222 401T188 301V215Q188 154 222 116T320 78Q368 78 398 100T447
|
||||
160L527 111Q502 57 451 23T319 -12Z" />
|
||||
<glyph unicode="d" glyph-name="d" d="M412 88H406Q383 41 345 15T250 -12Q159 -12 105 56T51 258Q51 392 105 460T250 528Q307 528 345 502T406 428H412V740H521V0H412V88ZM295 77Q319 77 340 83T377 101T402 130T412 172V344Q412 368 403 385T377 415T340 433T295
|
||||
439Q233 439 200 401T166 300V216Q166 153 199 115T295 77Z" />
|
||||
<glyph unicode="e" glyph-name="e" d="M311 -12Q252 -12 205 7T126 61T77 145T60 257Q60 319 77 369T126 454T203 509T303 528Q357 528 401 510T476 457T523 376T540 272V231H170V214Q170 153 208 115T313 76Q363 76 398 97T456 155L524 95Q498 50 445 19T311
|
||||
-12ZM303 445Q274 445 250 435T208 407T180 363T170 308V301H429V311Q429 372 395 408T303 445Z" />
|
||||
<glyph unicode="f" glyph-name="f" d="M78 88H243V428H68V516H243V609Q243 667 275 703T375 740H549V652H352V516H549V428H352V88H526V0H78V88Z" />
|
||||
<glyph unicode="g" glyph-name="g" d="M570 -56Q570 -136 503 -174T299 -212Q232 -212 187 -204T113 -179T72 -140T59 -88Q59 -46 82 -23T149 12V22Q123 32 108 50T92 97Q92 135 118 154T185 184V189Q138 211 112 251T86 347Q86 388 101 421T143 478T209 515T296
|
||||
528Q350 528 393 511V528Q393 558 410 578T462 598H556V512H436V488Q470 465 488 430T506 347Q506 306 491 273T449 216T383 180T296 167Q262 167 232 173Q214 168 196 156T178 123Q178 99 201 93T258 87H374Q478 87 524 48T570 -56ZM468 -63Q468 -37 447 -22T371
|
||||
-6H192Q154 -24 154 -64Q154 -93 178 -114T260 -135H341Q403 -135 435 -117T468 -63ZM296 242Q349 242 374 267T399 334V361Q399 403 374 428T296 453Q243 453 218 428T193 361V334Q193 292 218 267T296 242Z" />
|
||||
<glyph unicode="h" glyph-name="h" d="M84 740H193V428H198Q206 448 218 466T249 498T291 520T347 528Q425 528 473 477T521 332V0H412V316Q412 439 305 439Q284 439 264 434T228 417T203 389T193 350V0H84V740Z" />
|
||||
<glyph unicode="i" glyph-name="i" d="M332 610Q290 610 274 627T257 669V690Q257 715 273 732T331 749Q373 749 389 732T406 690V669Q406 644 390 627T332 610ZM101 88H277V428H101V516H386V88H551V0H101V88Z" />
|
||||
<glyph unicode="j" glyph-name="j" d="M98 -112H326V428H92V516H435V-69Q435 -127 403 -163T302 -200H98V-112ZM381 610Q339 610 323 627T306 669V690Q306 715 322 732T380 749Q422 749 438 732T455 690V669Q455 644 439 627T381 610Z" />
|
||||
<glyph unicode="k" glyph-name="k" d="M88 740H197V293H202L283 376L428 516H557L348 313L580 0H449L269 251L197 184V0H88V740Z" />
|
||||
<glyph unicode="l" glyph-name="l" d="M75 88H246V652H75V740H355V88H526V0H75V88Z" />
|
||||
<glyph unicode="m" glyph-name="m" d="M44 0V516H137V449H142Q154 482 176 505T241 528Q282 528 304 506T333 444H337Q351 479 376 503T448 528Q508 528 531 486T555 363V0H462V349Q462 403 449 423T407 444Q381 444 364 427T346 374V0H253V349Q253 403 241 423T199
|
||||
444Q172 444 155 427T137 374V0H44Z" />
|
||||
<glyph unicode="n" glyph-name="n" d="M84 0V516H193V428H198Q206 448 218 466T249 498T291 520T347 528Q425 528 473 477T521 332V0H412V316Q412 439 305 439Q284 439 264 434T228 417T203 389T193 350V0H84Z" />
|
||||
<glyph unicode="o" glyph-name="o" d="M300 -12Q243 -12 198 7T121 61T72 146T55 258Q55 320 72 370T120 455T197 509T300 528Q357 528 402 509T479 455T528 370T545 258Q545 196 528 146T480 61T403 7T300 -12ZM300 74Q360 74 395 110T431 219V297Q431 369 396
|
||||
405T300 442Q240 442 205 406T169 297V219Q169 147 204 111T300 74Z" />
|
||||
<glyph unicode="p" glyph-name="p" d="M79 516H188V428H194Q217 475 255 501T350 528Q441 528 495 460T549 258Q549 124 495 56T350 -12Q293 -12 255 14T194 88H188V-200H79V516ZM305 77Q367 77 400 115T434 216V300Q434 363 401 401T305 439Q281 439 260 433T223
|
||||
415T198 386T188 344V172Q188 148 197 131T223 101T260 83T305 77Z" />
|
||||
<glyph unicode="q" glyph-name="q" d="M412 88H406Q383 41 345 15T250 -12Q159 -12 105 56T51 258Q51 392 105 460T250 528Q307 528 345 502T406 428H412V516H521V-200H412V88ZM295 77Q319 77 340 83T377 101T402 130T412 172V344Q412 368 403 385T377 415T340
|
||||
433T295 439Q233 439 200 401T166 300V216Q166 153 199 115T295 77Z" />
|
||||
<glyph unicode="r" glyph-name="r" d="M76 88H213V428H76V516H322V379H328Q335 406 348 431T381 474T430 504T498 516H560V412H458Q398 412 360 377T322 281V88H509V0H76V88Z" />
|
||||
<glyph unicode="s" glyph-name="s" d="M301 -12Q217 -12 157 15T59 87L126 147Q160 110 202 91T303 71Q356 71 388 89T421 145Q421 161 415 172T398 190T373 201T342 207L260 220Q230 224 199 233T144 258T104 301T88 367Q88 446 148 487T308 528Q380 528 432
|
||||
507T520 447L459 383Q439 406 402 425T304 445Q195 445 195 376Q195 343 218 331T274 314L356 301Q387 296 417 288T472 264T512 221T528 155Q528 77 468 33T301 -12Z" />
|
||||
<glyph unicode="t" glyph-name="t" d="M331 0Q263 0 231 36T199 131V428H40V516H153Q182 516 194 527T206 569V698H308V516H528V428H308V88H528V0H331Z" />
|
||||
<glyph unicode="u" glyph-name="u" d="M407 88H402Q394 68 382 50T351 18T309 -4T253 -12Q175 -12 127 39T79 184V516H188V200Q188 77 295 77Q316 77 336 82T372 99T397 126T407 166V516H516V0H407V88Z" />
|
||||
<glyph unicode="v" glyph-name="v" d="M233 0L48 516H161L232 298L299 91H305L372 298L443 516H552L367 0H233Z" />
|
||||
<glyph unicode="w" glyph-name="w" d="M20 516H113L163 82H172L249 516H355L431 82H440L491 516H580L508 0H374L305 400H296L226 0H92L20 516Z" />
|
||||
<glyph unicode="x" glyph-name="x" d="M45 0L237 262L53 516H180L248 418L302 340H308L361 418L429 516H547L362 266L556 0H428L351 111L298 187H292L240 111L164 0H45Z" />
|
||||
<glyph unicode="y" glyph-name="y" d="M451 516H561L311 -100Q291 -150 259 -175T165 -200H78V-112H200L247 7L39 516H153L235 300L299 123H305L369 300L451 516Z" />
|
||||
<glyph unicode="z" glyph-name="z" d="M77 0V96L391 428H88V516H513V420L199 88H523V0H77Z" />
|
||||
<glyph unicode="{" glyph-name="braceleft" d="M324 -138Q271 -138 247 -112T222 -45V10Q222 36 227 56T240 91T259 119T278 142Q295 162 301 176T308 207Q308 272 183 272H100V350H183Q308 350 308 415Q308 431 302 445T278 480Q269 491 259 503T241 531T228
|
||||
566T222 612V667Q222 707 246 733T324 760H500V682H312V618Q312 598 315 584T323 557T336 535T352 515Q369 494 384 470T399 414Q399 372 370 347T286 314V308Q340 301 369 276T399 208Q399 176 384 152T352 107Q344 97 337 87T324 65T315 39T312 4V-60H500V-138H324Z"
|
||||
/>
|
||||
<glyph unicode="|" glyph-name="bar" d="M253 -138V760H347V-138H253Z" />
|
||||
<glyph unicode="}" glyph-name="braceright" d="M276 760Q329 760 353 734T378 667V612Q378 586 373 566T359 531T341 504T322 480Q305 460 299 446T292 415Q292 350 417 350H500V272H417Q292 272 292 207Q292 191 298 177T322 142Q331 130 341 118T359 91T373
|
||||
56T378 10V-45Q378 -85 354 -111T276 -138H100V-60H288V4Q288 44 276 65T248 107Q231 128 216 152T201 208Q201 250 230 275T314 308V314Q260 321 231 346T201 414Q201 446 216 470T248 515Q264 535 276 556T288 618V682H100V760H276Z" />
|
||||
<glyph unicode="~" glyph-name="asciitilde" d="M408 225Q377 225 350 235T293 260Q267 273 244 283T198 294Q171 294 155 276T125 225L45 256Q59 313 96 349T192 385Q223 385 250 375T307 350Q333 337 356 327T402 316Q429 316 445 334T475 385L555 354Q541 297
|
||||
504 261T408 225Z" />
|
||||
<glyph unicode=" " glyph-name="uni00A0" />
|
||||
<glyph unicode="¡" glyph-name="exclamdown" d="M240 -182V21L281 279H319L360 21V-182H240ZM300 368Q254 368 235 387T216 435V459Q216 488 235 507T300 526Q346 526 365 507T384 459V435Q384 406 365 387T300 368Z" />
|
||||
<glyph unicode="¢" glyph-name="cent" d="M270 -114V-8Q176 7 125 77T74 258Q74 368 125 438T270 524V630H354V526Q417 518 457 486T519 406L437 362Q426 391 404 411T350 438V77Q385 84 410 105T450 158L527 111Q505 63 463 31T354 -10V-114H270ZM183 215Q183
|
||||
164 206 128T275 80V436Q230 424 207 388T183 301V215Z" />
|
||||
<glyph unicode="£" glyph-name="sterling" d="M72 0V118Q114 135 134 167T154 245Q154 262 151 279H49V365H125Q113 397 103 429T93 501Q93 547 110 585T158 652T234 695T333 710Q411 710 465 681T554 600L476 535Q450 571 417 592T332 613Q275 613 241 583T207
|
||||
488Q207 453 216 424T236 365H426V279H262Q263 272 263 266T264 253Q264 224 257 201T238 160T212 129T183 107V100H544V0H72Z" />
|
||||
<glyph unicode="¥" glyph-name="yen" d="M67 87H247V241H67V328H192L13 698H132L298 328H305L471 698H587L408 328H533V241H353V87H533V0H67V87Z" />
|
||||
<glyph unicode="¦" glyph-name="brokenbar" d="M253 401V760H347V401H253ZM253 -138V221H347V-138H253Z" />
|
||||
<glyph unicode="§" glyph-name="section" d="M497 3Q497 -35 482 -66T439 -119T373 -152T287 -164Q239 -164 191 -150T104 -101L162 -32Q187 -55 218 -67T288 -80Q339 -80 368 -59T398 0Q398 33 373 52T300 85L230 105Q146 128 110 168T74 258Q74 304 102
|
||||
340T185 398V408Q145 433 127 467T108 543Q108 581 123 612T166 664T232 698T318 710Q366 710 414 696T501 647L443 578Q418 601 387 613T317 626Q266 626 237 605T207 546Q207 513 232 494T305 461L375 441Q459 418 495 378T531 288Q531 242 503 206T420 148V138Q460
|
||||
113 478 79T497 3ZM433 258Q433 294 410 318T329 358L260 376Q251 378 243 381T227 387Q201 367 187 343T172 288Q172 252 195 228T276 188L345 170Q354 168 362 165T378 159Q404 179 418 203T433 258Z" />
|
||||
<glyph unicode="¨" glyph-name="dieresis" d="M200 607Q163 607 148 622T133 660V679Q133 702 148 717T200 732Q237 732 252 717T267 679V660Q267 637 252 622T200 607ZM400 607Q363 607 348 622T333 660V679Q333 702 348 717T400 732Q437 732 452 717T467
|
||||
679V660Q467 637 452 622T400 607Z" />
|
||||
<glyph unicode="©" glyph-name="copyright" d="M300 18Q237 18 183 41T88 107T24 211T0 349Q0 425 23 486T87 591T182 657T300 680Q363 680 417 657T512 591T576 487T600 349Q600 272 577 211T513 107T418 41T300 18ZM300 73Q351 73 394 92T469 144T519 223T537
|
||||
319V379Q537 430 519 475T470 553T395 606T300 625Q249 625 206 606T131 554T81 475T63 379V319Q63 268 81 223T130 145T205 92T300 73ZM307 163Q229 163 185 213T140 349Q140 434 185 484T307 535Q361 535 394 510T444 445L378 410Q367 434 351 448T308 462Q273
|
||||
462 253 440T233 380V317Q233 281 252 259T309 236Q338 236 355 251T385 289L450 253Q433 215 399 189T307 163Z" />
|
||||
<glyph unicode="ª" glyph-name="ordfeminine" d="M429 350Q397 350 380 365T361 407H357Q346 379 319 361T246 342Q192 342 163 369T133 445Q133 499 173 525T288 552H355V575Q355 648 283 648Q249 648 227 634T192 599L144 640Q160 668 196 689T290 710Q358
|
||||
710 396 677T435 579V414H474V350H429ZM288 501Q252 501 233 490T213 456V444Q213 421 228 411T270 400Q305 400 330 415T355 460V501H288Z" />
|
||||
<glyph unicode="«" glyph-name="guillemotleft" d="M528 47L324 219V321L528 493L561 420L421 270L561 120L528 47ZM508 47L304 219V321L508 493L541 420L401 270L541 120L508 47Z" />
|
||||
<glyph unicode="¬" glyph-name="logicalnot" d="M431 68V261H62V350H522V68H431Z" />
|
||||
<glyph unicode="­" glyph-name="uni00AD" d="M149 250V359H451V250H149Z" />
|
||||
<glyph unicode="®" glyph-name="registered" d="M300 346Q262 346 229 360T170 398T131 455T117 528Q117 567 131 600T170 658T228 696T300 710Q338 710 371 696T429 658T468 601T483 528Q483 489 469 456T430 398T372 360T300 346ZM300 387Q329 387 353
|
||||
396T396 423T424 464T435 518V538Q435 568 425 592T396 633T354 659T300 669Q271 669 247 660T204 633T176 592T165 538V518Q165 488 175 464T204 423T246 397T300 387ZM273 436H227V621H315Q347 621 363 605T379 561Q379 541 370 528T343 507L386 436H335L299
|
||||
499H273V436ZM304 533Q330 533 330 555V563Q330 585 304 585H273V533H304Z" />
|
||||
<glyph unicode="¯" glyph-name="overscore" d="M155 710H445V628H155V710Z" />
|
||||
<glyph unicode="°" glyph-name="degree" d="M300 354Q262 354 230 367T173 405T136 461T122 532Q122 570 135 602T173 659T229 696T300 710Q337 710 369 697T426 659T464 603T478 532Q478 494 464 462T426 405T370 368T300 354ZM300 431Q343 431 369 460T395
|
||||
532Q395 575 369 604T300 633Q257 633 231 604T205 532Q205 489 231 460T300 431Z" />
|
||||
<glyph unicode="±" glyph-name="plusminus" d="M251 164V350H62V439H251V625H349V439H538V350H349V164H251ZM62 0V89H538V0H62Z" />
|
||||
<glyph unicode="²" glyph-name="twosuperior" d="M443 329H164V399L285 495Q318 521 333 541T349 586V590Q349 614 334 626T293 638Q263 638 248 623T225 585L156 611Q170 649 204 676T300 704Q364 704 399 673T434 593Q434 570 426 550T404 513T371 481T331
|
||||
450L250 393H443V329Z" />
|
||||
<glyph unicode="³" glyph-name="threesuperior" d="M283 551Q317 551 332 563T348 594V598Q348 618 333 630T292 642Q240 642 210 598L159 642Q180 670 211 687T294 704Q357 704 394 678T432 606Q432 571 410 550T355 523V520Q390 514 414 492T438 432Q438
|
||||
382 398 353T290 323Q231 323 198 344T146 393L205 437Q218 414 237 400T290 385Q322 385 338 398T355 436V440Q355 465 337 476T285 487H246V551H283Z" />
|
||||
<glyph unicode="´" glyph-name="acute" d="M311 579L244 611L335 791L429 745L311 579Z" />
|
||||
<glyph unicode="µ" glyph-name="mu" d="M84 -200V516H193V204Q193 77 297 77Q318 77 338 82T373 99T397 126T407 166V516H516V0H407V88H402Q394 68 384 50T357 18T320 -4T272 -12Q241 -12 219 -1T181 36H176L193 -69V-200H84Z" />
|
||||
<glyph unicode="¶" glyph-name="paragraph" d="M260 246Q213 246 173 263T101 310T53 381T35 472Q35 521 52 562T101 634T172 681T260 698H524V-149H432V612H352V-149H260V246Z" />
|
||||
<glyph unicode="·" glyph-name="middot" d="M300 222Q252 222 233 242T213 291V317Q213 346 232 366T300 386Q348 386 367 366T387 317V291Q387 262 368 242T300 222Z" />
|
||||
<glyph unicode="¸" glyph-name="cedilla" d="M313 -209Q268 -209 242 -195T203 -167L245 -120Q254 -131 269 -139T308 -148Q327 -148 339 -141T352 -119Q352 -107 338 -96T281 -79L256 -76L276 24H333L316 -58L320 -62Q331 -59 341 -57T363 -55Q392 -55 412
|
||||
-71T433 -123Q433 -146 423 -162T397 -188T359 -204T313 -209Z" />
|
||||
<glyph unicode="¹" glyph-name="onesuperior" d="M181 329V391H283V628L189 581L158 637L278 698H362V391H453V329H181Z" />
|
||||
<glyph unicode="º" glyph-name="ordmasculine" d="M300 342Q221 342 176 391T131 526Q131 612 176 661T300 710Q379 710 424 661T469 526Q469 440 424 391T300 342ZM300 405Q341 405 362 430T384 497V555Q384 597 363 622T300 647Q259 647 238 622T216 555V497Q216
|
||||
455 237 430T300 405Z" />
|
||||
<glyph unicode="»" glyph-name="guillemotright" d="M60 120L200 270L60 420L93 493L297 321V219L93 47L60 120ZM296 120L436 270L296 420L329 493L533 321V219L329 47L296 120Z" />
|
||||
<glyph unicode="¼" glyph-name="onequarter" d="M39 421H129V636L45 593L18 643L124 698H202V421H282V363H39V421ZM504 698H589L424 398H339L504 698ZM176 300H261L96 0H11L176 300ZM471 64H313V129L450 335H541V117H587V64H541V0H471V64ZM471 117V269H467L365
|
||||
117H471Z" />
|
||||
<glyph unicode="½" glyph-name="onehalf" d="M39 421H129V636L45 593L18 643L124 698H202V421H282V363H39V421ZM504 698H589L424 398H339L504 698ZM176 300H261L96 0H11L176 300ZM326 65L436 151Q466 175 478 194T491 237V241Q491 259 478 270T443 281Q418
|
||||
281 404 267T380 230L318 254Q324 271 334 286T359 314T397 333T448 341Q507 341 538 313T569 242Q569 221 562 204T541 170T510 138T472 108L405 58H578V0H326V65Z" />
|
||||
<glyph unicode="¾" glyph-name="threequarters" d="M146 357Q92 357 62 378T15 426L69 463Q80 442 98 428T145 414Q172 414 186 426T201 458V465Q201 486 184 497T136 508H105V563H138Q168 563 182 574T196 601V609Q196 626 183 636T146 647Q99 647 71 606L25
|
||||
646Q45 673 73 688T147 704Q204 704 238 680T272 615Q272 583 252 564T202 539V535Q235 530 256 511T278 456Q278 411 242 384T146 357ZM504 698H589L424 398H339L504 698ZM176 300H261L96 0H11L176 300ZM471 64H313V129L450 335H541V117H587V64H541V0H471V64ZM471
|
||||
117V269H467L365 117H471Z" />
|
||||
<glyph unicode="¿" glyph-name="questiondown" d="M303 -194Q253 -194 213 -180T143 -141T99 -79T83 2Q83 47 99 82T141 142T200 183T269 205V293H374V138Q293 135 246 106T199 13V-1Q199 -51 228 -75T307 -99Q359 -99 391 -70T435 5L530 -33Q521 -65 503
|
||||
-94T457 -145T391 -180T303 -194ZM319 368Q273 368 254 387T235 435V459Q235 488 254 507T319 526Q365 526 384 507T403 459V435Q403 406 384 387T319 368Z" />
|
||||
<glyph unicode="À" glyph-name="Agrave" d="M462 0L409 186H187L134 0H20L224 698H377L581 0H462ZM302 585H294L208 279H388L302 585ZM171 915L265 961L356 781L289 749L171 915Z" />
|
||||
<glyph unicode="Á" glyph-name="Aacute" d="M462 0L409 186H187L134 0H20L224 698H377L581 0H462ZM302 585H294L208 279H388L302 585ZM311 749L244 781L335 961L429 915L311 749Z" />
|
||||
<glyph unicode="Â" glyph-name="Acircumflex" d="M462 0L409 186H187L134 0H20L224 698H377L581 0H462ZM302 585H294L208 279H388L302 585ZM355 944L475 794L417 752L299 868L180 752L125 794L245 944H355Z" />
|
||||
<glyph unicode="Ã" glyph-name="Atilde" d="M462 0L409 186H187L134 0H20L224 698H377L581 0H462ZM302 585H294L208 279H388L302 585ZM374 774Q349 774 331 781T296 797Q273 808 256 814T222 820Q205 820 192 814T164 794L123 846Q138 869 163 886T226 903Q251
|
||||
903 269 896T304 880Q327 869 344 863T378 857Q395 857 408 863T436 883L477 831Q462 808 437 791T374 774Z" />
|
||||
<glyph unicode="Ä" glyph-name="Adieresis" d="M462 0L409 186H187L134 0H20L224 698H377L581 0H462ZM302 585H294L208 279H388L302 585ZM200 777Q163 777 148 792T133 830V849Q133 872 148 887T200 902Q237 902 252 887T267 849V830Q267 807 252 792T200
|
||||
777ZM400 777Q363 777 348 792T333 830V849Q333 872 348 887T400 902Q437 902 452 887T467 849V830Q467 807 452 792T400 777Z" />
|
||||
<glyph unicode="Å" glyph-name="Aring" d="M462 0L409 186H187L134 0H20L224 698H377L581 0H462ZM302 585H294L208 279H388L302 585ZM300 735Q273 735 249 744T208 770T181 810T171 861Q171 889 181 912T208 952T249 978T300 988Q327 988 351 979T392 952T419
|
||||
912T429 861Q429 834 419 811T392 771T351 745T300 735ZM300 795Q355 795 355 848V875Q355 928 300 928Q245 928 245 875V848Q245 795 300 795Z" />
|
||||
<glyph unicode="Æ" glyph-name="AE" d="M298 187H166L120 0H10L196 698H565V611H403V397H554V310H403V87H565V0H298V187ZM271 620L186 274H298V620H271Z" />
|
||||
<glyph unicode="Ç" glyph-name="Ccedilla" d="M319 617Q245 617 211 561T177 408V290Q177 242 185 204T211 138T255 96T319 81Q349 81 369 90T405 114T430 150T448 195L549 161Q537 126 520 95T476 41T415 4T331 -12L322 -58L326 -62Q337 -59 347 -57T368
|
||||
-55Q398 -55 418 -71T439 -123Q439 -146 429 -162T403 -188T365 -204T319 -209Q273 -209 247 -195T209 -167L251 -120Q260 -131 275 -139T314 -148Q333 -148 345 -141T358 -119Q358 -107 344 -96T287 -79L262 -76L275 -9Q164 7 111 98T58 349Q58 522 122 616T320
|
||||
710Q371 710 408 697T473 660T518 605T549 537L448 503Q440 527 430 548T405 584T370 608T319 617Z" />
|
||||
<glyph unicode="È" glyph-name="Egrave" d="M175 915L269 961L360 781L293 749L175 915ZM86 0V698H523V604H198V401H511V307H198V94H523V0H86Z" />
|
||||
<glyph unicode="É" glyph-name="Eacute" d="M86 0V698H523V604H198V401H511V307H198V94H523V0H86ZM315 749L248 781L339 961L433 915L315 749Z" />
|
||||
<glyph unicode="Ê" glyph-name="Ecircumflex" d="M359 944L479 794L421 752L303 868L184 752L129 794L249 944H359ZM86 0V698H523V604H198V401H511V307H198V94H523V0H86Z" />
|
||||
<glyph unicode="Ë" glyph-name="Edieresis" d="M86 0V698H523V604H198V401H511V307H198V94H523V0H86ZM204 777Q167 777 152 792T137 830V849Q137 872 152 887T204 902Q241 902 256 887T271 849V830Q271 807 256 792T204 777ZM404 777Q367 777 352 792T337
|
||||
830V849Q337 872 352 887T404 902Q441 902 456 887T471 849V830Q471 807 456 792T404 777Z" />
|
||||
<glyph unicode="Ì" glyph-name="Igrave" d="M80 0V85H244V613H80V698H520V613H356V85H520V0H80ZM171 915L265 961L356 781L289 749L171 915Z" />
|
||||
<glyph unicode="Í" glyph-name="Iacute" d="M80 0V85H244V613H80V698H520V613H356V85H520V0H80ZM311 749L244 781L335 961L429 915L311 749Z" />
|
||||
<glyph unicode="Î" glyph-name="Icircumflex" d="M80 0V85H244V613H80V698H520V613H356V85H520V0H80ZM355 944L475 794L417 752L299 868L180 752L125 794L245 944H355Z" />
|
||||
<glyph unicode="Ï" glyph-name="Idieresis" d="M80 0V85H244V613H80V698H520V613H356V85H520V0H80ZM200 777Q163 777 148 792T133 830V849Q133 872 148 887T200 902Q237 902 252 887T267 849V830Q267 807 252 792T200 777ZM400 777Q363 777 348 792T333 830V849Q333
|
||||
872 348 887T400 902Q437 902 452 887T467 849V830Q467 807 452 792T400 777Z" />
|
||||
<glyph unicode="Ð" glyph-name="Eth" d="M88 326H17V411H88V698H290Q424 698 488 609T553 349Q553 178 489 89T290 0H88V326ZM283 92Q360 92 397 143T435 291V407Q435 503 398 554T283 606H199V408H327V328H199V92H283Z" />
|
||||
<glyph unicode="Ñ" glyph-name="Ntilde" d="M374 774Q349 774 331 781T296 797Q273 808 256 814T222 820Q205 820 192 814T164 794L123 846Q138 869 163 886T226 903Q251 903 269 896T304 880Q327 869 344 863T378 857Q395 857 408 863T436 883L477 831Q462
|
||||
808 437 791T374 774ZM179 537H170V0H72V698H216L421 161H430V698H528V0H384L179 537Z" />
|
||||
<glyph unicode="Ò" glyph-name="Ograve" d="M171 915L265 961L356 781L289 749L171 915ZM300 -12Q234 -12 186 12T106 83T59 196T44 349Q44 434 59 501T105 615T185 685T300 710Q366 710 414 686T494 615T541 502T556 349Q556 263 541 196T495 83T415 13T300
|
||||
-12ZM300 81Q375 81 406 137T437 291V408Q437 504 406 560T300 617Q225 617 194 561T163 408V290Q163 194 194 138T300 81Z" />
|
||||
<glyph unicode="Ó" glyph-name="Oacute" d="M311 749L244 781L335 961L429 915L311 749ZM300 -12Q234 -12 186 12T106 83T59 196T44 349Q44 434 59 501T105 615T185 685T300 710Q366 710 414 686T494 615T541 502T556 349Q556 263 541 196T495 83T415 13T300
|
||||
-12ZM300 81Q375 81 406 137T437 291V408Q437 504 406 560T300 617Q225 617 194 561T163 408V290Q163 194 194 138T300 81Z" />
|
||||
<glyph unicode="Ô" glyph-name="Ocircumflex" d="M355 944L475 794L417 752L299 868L180 752L125 794L245 944H355ZM300 -12Q234 -12 186 12T106 83T59 196T44 349Q44 434 59 501T105 615T185 685T300 710Q366 710 414 686T494 615T541 502T556 349Q556 263
|
||||
541 196T495 83T415 13T300 -12ZM300 81Q375 81 406 137T437 291V408Q437 504 406 560T300 617Q225 617 194 561T163 408V290Q163 194 194 138T300 81Z" />
|
||||
<glyph unicode="Õ" glyph-name="Otilde" d="M300 -12Q234 -12 186 12T106 83T59 196T44 349Q44 434 59 501T105 615T185 685T300 710Q366 710 414 686T494 615T541 502T556 349Q556 263 541 196T495 83T415 13T300 -12ZM300 81Q375 81 406 137T437 291V408Q437
|
||||
504 406 560T300 617Q225 617 194 561T163 408V290Q163 194 194 138T300 81ZM374 774Q349 774 331 781T296 797Q273 808 256 814T222 820Q205 820 192 814T164 794L123 846Q138 869 163 886T226 903Q251 903 269 896T304 880Q327 869 344 863T378 857Q395 857 408
|
||||
863T436 883L477 831Q462 808 437 791T374 774Z" />
|
||||
<glyph unicode="Ö" glyph-name="Odieresis" d="M300 -12Q234 -12 186 12T106 83T59 196T44 349Q44 434 59 501T105 615T185 685T300 710Q366 710 414 686T494 615T541 502T556 349Q556 263 541 196T495 83T415 13T300 -12ZM300 81Q375 81 406 137T437 291V408Q437
|
||||
504 406 560T300 617Q225 617 194 561T163 408V290Q163 194 194 138T300 81ZM200 777Q163 777 148 792T133 830V849Q133 872 148 887T200 902Q237 902 252 887T267 849V830Q267 807 252 792T200 777ZM400 777Q363 777 348 792T333 830V849Q333 872 348 887T400
|
||||
902Q437 902 452 887T467 849V830Q467 807 452 792T400 777Z" />
|
||||
<glyph unicode="×" glyph-name="multiply" d="M300 241L145 85L80 150L236 305L80 460L145 525L300 369L455 525L520 460L364 305L520 150L455 85L300 241Z" />
|
||||
<glyph unicode="Ø" glyph-name="Oslash" d="M300 -12Q213 -12 159 29L110 -53L35 -10L98 94Q70 140 57 204T44 349Q44 434 59 501T105 615T185 685T300 710Q387 710 441 669L490 751L565 708L502 603Q530 557 543 494T556 349Q556 263 541 196T495 83T415
|
||||
13T300 -12ZM163 290Q163 266 164 245T171 203L392 580Q358 617 300 617Q225 617 194 561T163 408V290ZM300 81Q375 81 406 137T437 290V408Q437 432 436 453T429 495L208 118Q242 81 300 81Z" />
|
||||
<glyph unicode="Ù" glyph-name="Ugrave" d="M171 915L265 961L356 781L289 749L171 915ZM181 698V269Q181 226 183 191T198 132T234 94T300 81Q342 81 366 94T402 131T416 191T419 269V698H531V289Q531 214 523 158T489 64T419 7T300 -12Q227 -12 182 7T111
|
||||
63T78 157T69 289V698H181Z" />
|
||||
<glyph unicode="Ú" glyph-name="Uacute" d="M181 698V269Q181 226 183 191T198 132T234 94T300 81Q342 81 366 94T402 131T416 191T419 269V698H531V289Q531 214 523 158T489 64T419 7T300 -12Q227 -12 182 7T111 63T78 157T69 289V698H181ZM311 749L244
|
||||
781L335 961L429 915L311 749Z" />
|
||||
<glyph unicode="Û" glyph-name="Ucircumflex" d="M355 944L475 794L417 752L299 868L180 752L125 794L245 944H355ZM181 698V269Q181 226 183 191T198 132T234 94T300 81Q342 81 366 94T402 131T416 191T419 269V698H531V289Q531 214 523 158T489 64T419
|
||||
7T300 -12Q227 -12 182 7T111 63T78 157T69 289V698H181Z" />
|
||||
<glyph unicode="Ü" glyph-name="Udieresis" d="M181 698V269Q181 226 183 191T198 132T234 94T300 81Q342 81 366 94T402 131T416 191T419 269V698H531V289Q531 214 523 158T489 64T419 7T300 -12Q227 -12 182 7T111 63T78 157T69 289V698H181ZM200 777Q163
|
||||
777 148 792T133 830V849Q133 872 148 887T200 902Q237 902 252 887T267 849V830Q267 807 252 792T200 777ZM400 777Q363 777 348 792T333 830V849Q333 872 348 887T400 902Q437 902 452 887T467 849V830Q467 807 452 792T400 777Z" />
|
||||
<glyph unicode="Ý" glyph-name="Yacute" d="M244 0V263L12 698H137L226 524L298 375H304L377 524L466 698H588L356 263V0H244ZM311 749L244 781L335 961L429 915L311 749Z" />
|
||||
<glyph unicode="Þ" glyph-name="Thorn" d="M84 0V698H196V563H343Q443 563 495 507T548 354Q548 258 496 202T343 146H196V0H84ZM196 239H327Q432 239 432 333V376Q432 470 327 470H196V239Z" />
|
||||
<glyph unicode="ß" glyph-name="germandbls" d="M84 0V609Q84 667 116 703T217 740H363V654H191V516H531V426L373 212Q467 202 520 154T573 15Q573 -90 502 -145T311 -200H245V-114H307Q385 -114 423 -86T461 3V27Q461 87 421 112T294 138H275V224L427 430H191V0H84Z"
|
||||
/>
|
||||
<glyph unicode="à" glyph-name="agrave" d="M492 0Q445 0 422 24T393 87H388Q373 40 334 14T233 -12Q154 -12 107 29T60 144Q60 219 115 258T286 298H385V338Q385 443 274 443Q224 443 193 424T141 372L76 425Q98 467 150 497T284 528Q382 528 438 481T494
|
||||
345V86H559V0H492ZM264 68Q317 68 351 92T385 154V229H288Q170 229 170 158V138Q170 103 195 86T264 68ZM415 745L509 791L600 611L533 579L415 745Z" />
|
||||
<glyph unicode="á" glyph-name="aacute" d="M492 0Q445 0 422 24T393 87H388Q373 40 334 14T233 -12Q154 -12 107 29T60 144Q60 219 115 258T286 298H385V338Q385 443 274 443Q224 443 193 424T141 372L76 425Q98 467 150 497T284 528Q382 528 438 481T494
|
||||
345V86H559V0H492ZM264 68Q317 68 351 92T385 154V229H288Q170 229 170 158V138Q170 103 195 86T264 68ZM555 579L488 611L579 791L673 745L555 579Z" />
|
||||
<glyph unicode="â" glyph-name="acircumflex" d="M492 0Q445 0 422 24T393 87H388Q373 40 334 14T233 -12Q154 -12 107 29T60 144Q60 219 115 258T286 298H385V338Q385 443 274 443Q224 443 193 424T141 372L76 425Q98 467 150 497T284 528Q382 528 438 481T494
|
||||
345V86H559V0H492ZM264 68Q317 68 351 92T385 154V229H288Q170 229 170 158V138Q170 103 195 86T264 68ZM599 774L719 624L661 582L543 698L424 582L369 624L489 774H599Z" />
|
||||
<glyph unicode="ã" glyph-name="atilde" d="M492 0Q445 0 422 24T393 87H388Q373 40 334 14T233 -12Q154 -12 107 29T60 144Q60 219 115 258T286 298H385V338Q385 443 274 443Q224 443 193 424T141 372L76 425Q98 467 150 497T284 528Q382 528 438 481T494
|
||||
345V86H559V0H492ZM264 68Q317 68 351 92T385 154V229H288Q170 229 170 158V138Q170 103 195 86T264 68ZM618 604Q593 604 575 611T540 627Q517 638 500 644T466 650Q449 650 436 644T408 624L367 676Q382 699 407 716T470 733Q495 733 513 726T548 710Q571 699
|
||||
588 693T622 687Q639 687 652 693T680 713L721 661Q706 638 681 621T618 604Z" />
|
||||
<glyph unicode="ä" glyph-name="adieresis" d="M492 0Q445 0 422 24T393 87H388Q373 40 334 14T233 -12Q154 -12 107 29T60 144Q60 219 115 258T286 298H385V338Q385 443 274 443Q224 443 193 424T141 372L76 425Q98 467 150 497T284 528Q382 528 438 481T494
|
||||
345V86H559V0H492ZM264 68Q317 68 351 92T385 154V229H288Q170 229 170 158V138Q170 103 195 86T264 68ZM444 607Q407 607 392 622T377 660V679Q377 702 392 717T444 732Q481 732 496 717T511 679V660Q511 637 496 622T444 607ZM644 607Q607 607 592 622T577 660V679Q577
|
||||
702 592 717T644 732Q681 732 696 717T711 679V660Q711 637 696 622T644 607Z" />
|
||||
<glyph unicode="å" glyph-name="aring" d="M492 0Q445 0 422 24T393 87H388Q373 40 334 14T233 -12Q154 -12 107 29T60 144Q60 219 115 258T286 298H385V338Q385 443 274 443Q224 443 193 424T141 372L76 425Q98 467 150 497T284 528Q382 528 438 481T494
|
||||
345V86H559V0H492ZM264 68Q317 68 351 92T385 154V229H288Q170 229 170 158V138Q170 103 195 86T264 68ZM544 565Q517 565 493 574T452 600T425 640T415 691Q415 719 425 742T452 782T493 808T544 818Q571 818 595 809T636 782T663 742T673 691Q673 664 663 641T636
|
||||
601T595 575T544 565ZM544 625Q599 625 599 678V705Q599 758 544 758Q489 758 489 705V678Q489 625 544 625Z" />
|
||||
<glyph unicode="æ" glyph-name="ae" d="M139 -12Q83 -12 45 26T7 139Q7 219 53 259T187 299H241V354Q241 407 223 430T169 454Q136 454 118 434T88 380L18 413Q35 464 73 496T171 528Q216 528 247 509T295 452H299Q319 493 350 510T421 528Q499 528 541 467T583
|
||||
294V234H333V213Q333 147 355 107T419 66Q453 66 471 90T501 147L576 120Q568 94 555 71T523 29T477 -1T418 -12Q367 -12 328 17T268 103H264Q253 43 220 16T139 -12ZM161 62Q203 62 222 91T241 165V234H198Q150 234 125 215T99 154V137Q99 102 113 82T161 62ZM416
|
||||
456Q375 456 354 426T333 338V299H498V338Q498 395 477 425T416 456Z" />
|
||||
<glyph unicode="ç" glyph-name="ccedilla" d="M318 528Q398 528 446 494T519 406L434 360Q420 396 392 417T318 438Q256 438 222 401T188 301V215Q188 154 222 116T320 78Q368 78 398 100T447 160L527 111Q503 57 455 24T328 -12L318 -58L322 -62Q333 -59
|
||||
343 -57T365 -55Q394 -55 414 -71T435 -123Q435 -146 425 -162T399 -188T361 -204T315 -209Q270 -209 244 -195T206 -167L247 -120Q256 -131 271 -139T310 -148Q329 -148 341 -141T354 -119Q354 -107 341 -96T283 -79L258 -76L272 -8Q178 6 126 77T74 258Q74 320
|
||||
90 370T138 455T215 509T318 528Z" />
|
||||
<glyph unicode="è" glyph-name="egrave" d="M311 -12Q252 -12 205 7T126 61T77 145T60 257Q60 319 77 369T126 454T203 509T303 528Q357 528 401 510T476 457T523 376T540 272V231H170V214Q170 153 208 115T313 76Q363 76 398 97T456 155L524 95Q498 50 445
|
||||
19T311 -12ZM303 445Q274 445 250 435T208 407T180 363T170 308V301H429V311Q429 372 395 408T303 445ZM172 745L266 791L357 611L290 579L172 745Z" />
|
||||
<glyph unicode="é" glyph-name="eacute" d="M312 579L245 611L336 791L430 745L312 579ZM311 -12Q252 -12 205 7T126 61T77 145T60 257Q60 319 77 369T126 454T203 509T303 528Q357 528 401 510T476 457T523 376T540 272V231H170V214Q170 153 208 115T313
|
||||
76Q363 76 398 97T456 155L524 95Q498 50 445 19T311 -12ZM303 445Q274 445 250 435T208 407T180 363T170 308V301H429V311Q429 372 395 408T303 445Z" />
|
||||
<glyph unicode="ê" glyph-name="ecircumflex" d="M356 774L476 624L418 582L300 698L181 582L126 624L246 774H356ZM311 -12Q252 -12 205 7T126 61T77 145T60 257Q60 319 77 369T126 454T203 509T303 528Q357 528 401 510T476 457T523 376T540 272V231H170V214Q170
|
||||
153 208 115T313 76Q363 76 398 97T456 155L524 95Q498 50 445 19T311 -12ZM303 445Q274 445 250 435T208 407T180 363T170 308V301H429V311Q429 372 395 408T303 445Z" />
|
||||
<glyph unicode="ë" glyph-name="edieresis" d="M201 607Q164 607 149 622T134 660V679Q134 702 149 717T201 732Q238 732 253 717T268 679V660Q268 637 253 622T201 607ZM401 607Q364 607 349 622T334 660V679Q334 702 349 717T401 732Q438 732 453 717T468
|
||||
679V660Q468 637 453 622T401 607ZM311 -12Q252 -12 205 7T126 61T77 145T60 257Q60 319 77 369T126 454T203 509T303 528Q357 528 401 510T476 457T523 376T540 272V231H170V214Q170 153 208 115T313 76Q363 76 398 97T456 155L524 95Q498 50 445 19T311 -12ZM303
|
||||
445Q274 445 250 435T208 407T180 363T170 308V301H429V311Q429 372 395 408T303 445Z" />
|
||||
<glyph unicode="ì" glyph-name="igrave" d="M101 88H277V428H101V516H386V88H551V0H101V88ZM203 745L297 791L388 611L321 579L203 745Z" />
|
||||
<glyph unicode="í" glyph-name="iacute" d="M343 579L276 611L367 791L461 745L343 579ZM101 88H277V428H101V516H386V88H551V0H101V88Z" />
|
||||
<glyph unicode="î" glyph-name="icircumflex" d="M387 774L507 624L449 582L331 698L212 582L157 624L277 774H387ZM101 88H277V428H101V516H386V88H551V0H101V88Z" />
|
||||
<glyph unicode="ï" glyph-name="idieresis" d="M232 607Q195 607 180 622T165 660V679Q165 702 180 717T232 732Q269 732 284 717T299 679V660Q299 637 284 622T232 607ZM432 607Q395 607 380 622T365 660V679Q365 702 380 717T432 732Q469 732 484 717T499
|
||||
679V660Q499 637 484 622T432 607ZM101 88H277V428H101V516H386V88H551V0H101V88Z" />
|
||||
<glyph unicode="ð" glyph-name="eth" d="M469 702L387 648Q419 616 447 577T498 491T532 389T545 271Q545 199 528 146T480 57T403 5T303 -12Q244 -12 198 6T121 58T72 140T55 249Q55 307 70 353T113 433T179 484T264 502Q324 502 363 475T426 406L432 409Q412
|
||||
464 380 510T306 597L210 534L167 585L254 642Q220 670 183 694T103 740H271Q286 730 302 719T336 694L426 753L469 702ZM301 74Q361 74 396 109T432 213V276Q432 345 396 380T300 415Q240 415 205 380T169 276V213Q169 144 205 109T301 74Z" />
|
||||
<glyph unicode="ñ" glyph-name="ntilde" d="M84 0V516H193V428H198Q206 448 218 466T249 498T291 520T347 528Q425 528 473 477T521 332V0H412V316Q412 439 305 439Q284 439 264 434T228 417T203 389T193 350V0H84ZM376 604Q351 604 333 611T298 627Q275
|
||||
638 258 644T224 650Q207 650 194 644T166 624L125 676Q140 699 165 716T228 733Q253 733 271 726T306 710Q329 699 346 693T380 687Q397 687 410 693T438 713L479 661Q464 638 439 621T376 604Z" />
|
||||
<glyph unicode="ò" glyph-name="ograve" d="M300 -12Q243 -12 198 7T121 61T72 146T55 258Q55 320 72 370T120 455T197 509T300 528Q357 528 402 509T479 455T528 370T545 258Q545 196 528 146T480 61T403 7T300 -12ZM300 74Q360 74 395 110T431 219V297Q431
|
||||
369 396 405T300 442Q240 442 205 406T169 297V219Q169 147 204 111T300 74ZM171 745L265 791L356 611L289 579L171 745Z" />
|
||||
<glyph unicode="ó" glyph-name="oacute" d="M311 579L244 611L335 791L429 745L311 579ZM300 -12Q243 -12 198 7T121 61T72 146T55 258Q55 320 72 370T120 455T197 509T300 528Q357 528 402 509T479 455T528 370T545 258Q545 196 528 146T480 61T403 7T300
|
||||
-12ZM300 74Q360 74 395 110T431 219V297Q431 369 396 405T300 442Q240 442 205 406T169 297V219Q169 147 204 111T300 74Z" />
|
||||
<glyph unicode="ô" glyph-name="ocircumflex" d="M355 774L475 624L417 582L299 698L180 582L125 624L245 774H355ZM300 -12Q243 -12 198 7T121 61T72 146T55 258Q55 320 72 370T120 455T197 509T300 528Q357 528 402 509T479 455T528 370T545 258Q545 196
|
||||
528 146T480 61T403 7T300 -12ZM300 74Q360 74 395 110T431 219V297Q431 369 396 405T300 442Q240 442 205 406T169 297V219Q169 147 204 111T300 74Z" />
|
||||
<glyph unicode="õ" glyph-name="otilde" d="M300 -12Q243 -12 198 7T121 61T72 146T55 258Q55 320 72 370T120 455T197 509T300 528Q357 528 402 509T479 455T528 370T545 258Q545 196 528 146T480 61T403 7T300 -12ZM300 74Q360 74 395 110T431 219V297Q431
|
||||
369 396 405T300 442Q240 442 205 406T169 297V219Q169 147 204 111T300 74ZM374 604Q349 604 331 611T296 627Q273 638 256 644T222 650Q205 650 192 644T164 624L123 676Q138 699 163 716T226 733Q251 733 269 726T304 710Q327 699 344 693T378 687Q395 687 408
|
||||
693T436 713L477 661Q462 638 437 621T374 604Z" />
|
||||
<glyph unicode="ö" glyph-name="odieresis" d="M200 607Q163 607 148 622T133 660V679Q133 702 148 717T200 732Q237 732 252 717T267 679V660Q267 637 252 622T200 607ZM400 607Q363 607 348 622T333 660V679Q333 702 348 717T400 732Q437 732 452 717T467
|
||||
679V660Q467 637 452 622T400 607ZM300 -12Q243 -12 198 7T121 61T72 146T55 258Q55 320 72 370T120 455T197 509T300 528Q357 528 402 509T479 455T528 370T545 258Q545 196 528 146T480 61T403 7T300 -12ZM300 74Q360 74 395 110T431 219V297Q431 369 396 405T300
|
||||
442Q240 442 205 406T169 297V219Q169 147 204 111T300 74Z" />
|
||||
<glyph unicode="÷" glyph-name="divide" d="M62 261V350H538V261H62ZM300 42Q260 42 244 58T227 99V122Q227 146 243 162T300 179Q340 179 356 163T373 122V99Q373 75 357 59T300 42ZM300 432Q260 432 244 448T227 489V512Q227 536 243 552T300 569Q340 569
|
||||
356 553T373 512V489Q373 465 357 449T300 432Z" />
|
||||
<glyph unicode="ø" glyph-name="oslash" d="M37 -3L105 81Q55 149 55 258Q55 320 72 370T120 455T197 509T300 528Q384 528 441 488L503 564L563 519L495 435Q545 367 545 258Q545 196 528 146T480 61T403 7T300 -12Q216 -12 159 28L97 -48L37 -3ZM300 442Q240
|
||||
442 205 406T169 297V223Q169 206 170 190T177 161L385 415Q353 442 300 442ZM300 74Q360 74 395 110T431 219V293Q431 310 430 326T423 355L215 101Q231 87 252 81T300 74Z" />
|
||||
<glyph unicode="ù" glyph-name="ugrave" d="M407 88H402Q394 68 382 50T351 18T309 -4T253 -12Q175 -12 127 39T79 184V516H188V200Q188 77 295 77Q316 77 336 82T372 99T397 126T407 166V516H516V0H407V88ZM426 745L520 791L611 611L544 579L426 745Z" />
|
||||
<glyph unicode="ú" glyph-name="uacute" d="M566 579L499 611L590 791L684 745L566 579ZM407 88H402Q394 68 382 50T351 18T309 -4T253 -12Q175 -12 127 39T79 184V516H188V200Q188 77 295 77Q316 77 336 82T372 99T397 126T407 166V516H516V0H407V88Z" />
|
||||
<glyph unicode="û" glyph-name="ucircumflex" d="M610 774L730 624L672 582L554 698L435 582L380 624L500 774H610ZM407 88H402Q394 68 382 50T351 18T309 -4T253 -12Q175 -12 127 39T79 184V516H188V200Q188 77 295 77Q316 77 336 82T372 99T397 126T407
|
||||
166V516H516V0H407V88Z" />
|
||||
<glyph unicode="ü" glyph-name="udieresis" d="M455 607Q418 607 403 622T388 660V679Q388 702 403 717T455 732Q492 732 507 717T522 679V660Q522 637 507 622T455 607ZM655 607Q618 607 603 622T588 660V679Q588 702 603 717T655 732Q692 732 707 717T722
|
||||
679V660Q722 637 707 622T655 607ZM407 88H402Q394 68 382 50T351 18T309 -4T253 -12Q175 -12 127 39T79 184V516H188V200Q188 77 295 77Q316 77 336 82T372 99T397 126T407 166V516H516V0H407V88Z" />
|
||||
<glyph unicode="ý" glyph-name="yacute" d="M451 516H561L311 -100Q291 -150 259 -175T165 -200H78V-112H200L247 7L39 516H153L235 300L299 123H305L369 300L451 516ZM312 579L245 611L336 791L430 745L312 579Z" />
|
||||
<glyph unicode="þ" glyph-name="thorn" d="M79 740H188V428H194Q217 475 255 501T350 528Q441 528 495 460T549 258Q549 124 495 56T350 -12Q293 -12 255 14T194 88H188V-200H79V740ZM305 77Q367 77 400 115T434 216V300Q434 363 401 401T305 439Q281 439
|
||||
260 433T223 415T198 386T188 344V172Q188 148 197 131T223 101T260 83T305 77Z" />
|
||||
<glyph unicode="ÿ" glyph-name="ydieresis" d="M451 516H561L311 -100Q291 -150 259 -175T165 -200H78V-112H200L247 7L39 516H153L235 300L299 123H305L369 300L451 516ZM201 607Q164 607 149 622T134 660V679Q134 702 149 717T201 732Q238 732 253 717T268
|
||||
679V660Q268 637 253 622T201 607ZM401 607Q364 607 349 622T334 660V679Q334 702 349 717T401 732Q438 732 453 717T468 679V660Q468 637 453 622T401 607Z" />
|
||||
<glyph unicode="–" glyph-name="endash" d="M60 257V351H540V257H60Z" />
|
||||
<glyph unicode="—" glyph-name="emdash" d="M0 257V351H600V257H0Z" />
|
||||
<glyph unicode="‘" glyph-name="quoteleft" d="M306 740H390L336 442H181L306 740Z" />
|
||||
<glyph unicode="’" glyph-name="quoteright" d="M264 740H419L294 442H210L264 740Z" />
|
||||
<glyph unicode="‚" glyph-name="quotesinglbase" d="M244 152H399L275 -145H190L244 152Z" />
|
||||
<glyph unicode="“" glyph-name="quotedblleft" d="M443 740H527L473 442H318L443 740ZM433 740H517L463 442H308L433 740Z" />
|
||||
<glyph unicode="”" glyph-name="quotedblright" d="M401 740H556L431 442H347L401 740ZM391 740H546L421 442H337L391 740Z" />
|
||||
<glyph unicode="„" glyph-name="quotedblbase" d="M393 152H548L424 -145H339L393 152ZM383 152H538L414 -145H329L383 152Z" />
|
||||
<glyph unicode="•" glyph-name="bullet" d="M300 173Q226 173 196 206T165 289V319Q165 344 172 365T195 401T237 426T300 435Q337 435 363 426T405 402T428 365T435 319V289Q435 239 405 206T300 173Z" />
|
||||
<glyph unicode="‹" glyph-name="guilsinglleft" d="M375 47L171 219V321L375 493L408 420L268 270L408 120L375 47Z" />
|
||||
<glyph unicode="›" glyph-name="guilsinglright" d="M192 120L332 270L192 420L225 493L429 321V219L225 47L192 120Z" />
|
||||
</font>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 51 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,327 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<defs >
|
||||
<font id="IBMPlexMono" horiz-adv-x="600" ><font-face
|
||||
font-family="IBM Plex Mono SemiBold"
|
||||
units-per-em="1000"
|
||||
panose-1="2 11 7 9 5 2 3 0 2 3"
|
||||
ascent="1025"
|
||||
descent="-275"
|
||||
alphabetic="0" />
|
||||
<glyph unicode=" " glyph-name="space" />
|
||||
<glyph unicode="!" glyph-name="exclam" d="M279 240L230 495V698H370V495L321 240H279ZM300 -11Q250 -11 230 10T209 61V89Q209 119 229 140T300 161Q350 161 370 140T391 89V61Q391 31 371 10T300 -11Z" />
|
||||
<glyph unicode=""" glyph-name="quotedbl" d="M394 428V740H510V428H394ZM347 428V740H463V428H347Z" />
|
||||
<glyph unicode="#" glyph-name="numbersign" d="M126 201H11V297H144L161 401H49V497H179L214 698H318L195 0H91L126 201ZM405 698H509L474 497H589V401H456L439 297H551V201H421L386 0H282L405 698Z" />
|
||||
<glyph unicode="$" glyph-name="dollar" d="M261 -11Q183 -5 128 24T37 101L116 182Q182 108 268 98V305L245 309Q196 318 161 336T103 380T70 438T59 507Q59 592 111 644T261 707V811H359V708Q424 700 470 675T549 610L470 529Q424 584 352 596V406L377 402Q475
|
||||
384 519 333T563 206Q563 121 511 65T359 -7V-113H261V-11ZM182 511Q182 474 202 454T268 423V599Q182 587 182 511ZM441 198Q441 238 420 258T352 289V99Q397 107 419 132T441 198Z" />
|
||||
<glyph unicode="%" glyph-name="percent" d="M166 344Q94 344 53 392T12 527Q12 614 53 662T166 710Q238 710 279 662T320 527Q320 440 279 392T166 344ZM166 421Q198 421 212 443T227 504V550Q227 588 213 610T166 633Q134 633 120 611T105 550V504Q105 466 119
|
||||
444T166 421ZM492 698H593L439 398H339L492 698ZM161 300H261L108 0H7L161 300ZM434 -12Q362 -12 321 36T280 171Q280 258 321 306T434 354Q506 354 547 306T588 171Q588 84 547 36T434 -12ZM434 65Q466 65 480 87T495 148V194Q495 232 481 254T434 277Q402 277
|
||||
388 255T373 194V148Q373 110 387 88T434 65Z" />
|
||||
<glyph unicode="&" glyph-name="ampersand" d="M210 -12Q163 -12 128 4T67 47T30 110T17 187Q17 247 46 297T139 382Q107 424 91 459T74 536Q74 574 88 606T128 661T189 697T265 710Q301 710 331 700T384 674T424 636T448 591L357 543Q346 575 322 593T265
|
||||
612Q230 612 209 591T188 538V532Q188 517 192 504T205 475T230 439T268 391L334 307L390 229H396Q399 247 401 268T404 311T406 355T407 396H574V299H503Q497 262 489 228T463 154L591 0H452L370 99H363Q354 48 314 18T210 -12ZM242 89Q271 89 295 100T334 137L192
|
||||
315Q160 289 149 260T138 197V192Q138 142 166 116T242 89Z" />
|
||||
<glyph unicode="'" glyph-name="quotesingle" d="M242 428V740H358V428H242Z" />
|
||||
<glyph unicode="(" glyph-name="parenleft" d="M183 311Q183 384 199 451T244 577T311 681T391 760H519Q474 728 433 685T361 591T312 481T293 359V263Q293 200 311 142T361 32T433 -63T519 -138H391Q349 -107 311 -60T245 45T200 170T183 311Z" />
|
||||
<glyph unicode=")" glyph-name="parenright" d="M417 311Q417 238 401 171T356 45T289 -60T209 -138H81Q126 -106 167 -63T239 31T288 141T307 263V359Q307 422 289 480T239 590T167 685T81 760H209Q251 728 289 682T355 577T400 452T417 311Z" />
|
||||
<glyph unicode="*" glyph-name="asterisk" d="M186 44L96 105L213 261L34 322L67 422L244 360V549H356V360L533 422L566 322L387 261L504 105L414 44L300 200L186 44Z" />
|
||||
<glyph unicode="+" glyph-name="plus" d="M244 62V254H62V357H244V549H356V357H538V254H356V62H244Z" />
|
||||
<glyph unicode="," glyph-name="comma" d="M236 163H408L277 -148H180L236 163Z" />
|
||||
<glyph unicode="-" glyph-name="hyphen" d="M145 242V367H455V242H145Z" />
|
||||
<glyph unicode="." glyph-name="period" d="M300 -11Q248 -11 226 11T204 65V95Q204 127 226 149T300 171Q352 171 374 149T396 95V65Q396 33 374 11T300 -11Z" />
|
||||
<glyph unicode="/" glyph-name="slash" d="M78 -138L407 760H522L193 -138H78Z" />
|
||||
<glyph unicode="0" glyph-name="zero" d="M300 -12Q232 -12 182 12T100 83T52 196T36 349Q36 434 51 501T99 615T182 685T300 710Q436 710 500 615T564 349Q564 178 500 83T300 -12ZM300 93Q372 93 401 144T431 287V411Q431 502 402 553T300 605Q228 605 199 554T169
|
||||
411V287Q169 196 198 145T300 93ZM300 290Q265 290 252 304T238 338V360Q238 380 251 394T300 408Q335 408 348 394T362 360V338Q362 318 349 304T300 290Z" />
|
||||
<glyph unicode="1" glyph-name="one" d="M91 0V107H283V603H274L122 423L40 492L213 698H414V107H572V0H91Z" />
|
||||
<glyph unicode="2" glyph-name="two" d="M550 0H58V122L282 313Q338 361 365 400T392 485V495Q392 545 364 571T286 597Q231 597 202 567T160 493L46 536Q57 570 77 601T128 657T201 695T297 710Q353 710 396 695T470 652T515 586T531 503Q531 460 518 424T480
|
||||
356T424 293T353 233L201 110H550V0Z" />
|
||||
<glyph unicode="3" glyph-name="three" d="M272 416Q335 416 364 441T394 504V511Q394 554 366 578T287 602Q239 602 203 580T141 518L56 594Q74 618 96 639T147 676T211 701T291 710Q344 710 388 698T464 661T513 604T531 528Q531 495 520 468T491 421T448 388T395
|
||||
369V364Q426 358 453 345T500 310T532 260T544 193Q544 147 525 109T472 44T389 3T280 -12Q230 -12 191 -2T122 27T70 68T31 117L128 192Q153 150 187 123T282 96Q342 96 374 123T407 200V208Q407 256 372 281T273 307H203V416H272Z" />
|
||||
<glyph unicode="4" glyph-name="four" d="M351 0V135H28V247L313 698H476V237H570V135H476V0H351ZM135 237H351V570H343L135 237Z" />
|
||||
<glyph unicode="5" glyph-name="five" d="M518 584H198L181 350H189Q201 372 215 390T247 422T289 442T344 449Q388 449 427 434T495 391T540 322T557 229Q557 177 540 133T489 57T407 6T295 -12Q246 -12 208 -2T140 27T89 68T51 117L146 192Q158 172 172 155T205
|
||||
124T245 104T297 96Q357 96 388 128T420 218V226Q420 281 388 312T298 343Q254 343 227 327T182 291L75 306L101 698H518V584Z" />
|
||||
<glyph unicode="6" glyph-name="six" d="M303 -12Q242 -12 195 7T114 62T64 148T46 262Q46 337 69 403T128 525T210 625T300 698H478Q415 654 366 614T280 531T219 444T182 344L190 342Q200 362 214 380T247 413T292 436T352 445Q396 445 434 430T499 387T543
|
||||
318T559 228Q559 175 541 131T490 55T409 6T303 -12ZM302 91Q362 91 393 122T424 212V224Q424 282 393 313T302 344Q245 344 213 313T181 224V212Q181 154 212 123T302 91Z" />
|
||||
<glyph unicode="7" glyph-name="seven" d="M167 0L416 591H167V463H55V698H546V584L305 0H167Z" />
|
||||
<glyph unicode="8" glyph-name="eight" d="M300 -12Q238 -12 190 3T109 46T59 110T42 191Q42 260 80 302T182 361V369Q128 388 96 428T63 529Q63 569 79 602T125 660T199 697T300 710Q357 710 401 697T475 660T521 603T537 529Q537 468 505 428T418 369V361Q481
|
||||
344 519 302T558 191Q558 147 541 110T491 46T410 4T300 -12ZM300 90Q359 90 390 116T422 190V210Q422 258 391 284T300 311Q240 311 209 285T178 210V190Q178 143 209 117T300 90ZM300 408Q355 408 384 433T413 502V516Q413 559 384 583T300 608Q245 608 216 584T187
|
||||
516V502Q187 458 216 433T300 408Z" />
|
||||
<glyph unicode="9" glyph-name="nine" d="M554 436Q554 361 531 295T472 173T390 73T300 0H122Q185 44 234 84T320 167T381 254T418 354L410 356Q400 336 386 317T353 284T308 262T248 253Q204 253 166 268T100 311T57 380T41 470Q41 523 59 567T110 643T191 692T297
|
||||
710Q358 710 405 691T486 636T536 550T554 436ZM298 354Q355 354 387 385T419 474V486Q419 544 388 575T298 607Q238 607 207 576T176 486V474Q176 416 207 385T298 354Z" />
|
||||
<glyph unicode=":" glyph-name="colon" d="M300 -11Q248 -11 226 11T204 65V95Q204 127 226 149T300 171Q352 171 374 149T396 95V65Q396 33 374 11T300 -11ZM300 344Q248 344 226 366T204 420V450Q204 482 226 504T300 526Q352 526 374 504T396 450V420Q396 388
|
||||
374 366T300 344Z" />
|
||||
<glyph unicode=";" glyph-name="semicolon" d="M236 163H408L277 -148H180L236 163ZM300 344Q248 344 226 366T204 420V450Q204 482 226 504T300 526Q352 526 374 504T396 450V420Q396 388 374 366T300 344Z" />
|
||||
<glyph unicode="<" glyph-name="less" d="M85 245V365L515 601V479L201 309V301L515 131V9L85 245Z" />
|
||||
<glyph unicode="=" glyph-name="equal" d="M62 364V467H538V364H62ZM62 143V246H538V143H62Z" />
|
||||
<glyph unicode=">" glyph-name="greater" d="M85 131L399 301V309L85 479V601L515 365V245L85 9V131Z" />
|
||||
<glyph unicode="?" glyph-name="question" d="M217 228V382Q295 385 340 411T386 498V512Q386 557 359 578T290 600Q243 600 214 574T174 505L66 549Q75 580 93 609T138 660T204 696T294 710Q345 710 387 696T459 655T505 592T522 510Q522 466 507 432T467 372T408
|
||||
330T339 308V228H217ZM281 -11Q231 -11 211 10T190 61V89Q190 119 210 140T281 161Q331 161 351 140T372 89V61Q372 31 352 10T281 -11Z" />
|
||||
<glyph unicode="@" glyph-name="at" d="M449 -112H338Q257 -112 201 -87T111 -9T61 120T45 303Q45 418 62 496T111 621T191 689T301 710Q364 710 410 691T486 639T530 561T545 463V91H449V151H442Q431 117 408 99T346 80Q287 80 253 131T219 294Q219 405 253 456T346
|
||||
508Q384 508 407 489T442 437H449V463Q449 548 412 586T306 624Q267 624 237 611T187 566T157 484T146 358V229Q146 171 155 124T187 44T247 -8T338 -26H449V-112ZM385 157Q415 157 432 174T449 222V366Q449 397 432 414T385 431Q353 431 337 408T320 326V262Q320
|
||||
203 336 180T385 157Z" />
|
||||
<glyph unicode="A" glyph-name="A" d="M449 0L400 177H193L145 0H13L209 698H391L588 0H449ZM301 569H293L216 284H378L301 569Z" />
|
||||
<glyph unicode="B" glyph-name="B" d="M80 698H323Q422 698 475 649T529 514Q529 454 500 418T416 366V361Q479 346 515 307T552 195Q552 151 538 116T497 54T433 14T348 0H80V698ZM308 95Q363 95 390 115T418 186V219Q418 268 391 288T308 309H208V95H308ZM291
|
||||
400Q344 400 369 419T395 485V518Q395 565 370 584T291 603H208V400H291Z" />
|
||||
<glyph unicode="C" glyph-name="C" d="M318 -12Q181 -12 116 82T50 349Q50 521 115 615T318 710Q370 710 409 696T478 658T526 599T556 523L436 489Q429 513 420 534T398 570T364 594T316 603Q248 603 219 551T189 409V289Q189 199 218 147T316 95Q344 95 364
|
||||
103T397 127T420 164T436 209L556 175Q545 134 526 100T478 41T410 2T318 -12Z" />
|
||||
<glyph unicode="D" glyph-name="D" d="M73 698H290Q426 698 492 608T559 349Q559 179 493 90T290 0H73V698ZM281 105Q353 105 387 152T422 289V410Q422 499 388 546T281 593H202V105H281Z" />
|
||||
<glyph unicode="E" glyph-name="E" d="M83 0V698H524V590H214V408H513V300H214V108H524V0H83Z" />
|
||||
<glyph unicode="F" glyph-name="F" d="M83 0V698H539V590H214V408H507V300H214V0H83Z" />
|
||||
<glyph unicode="G" glyph-name="G" d="M421 97H413Q404 76 391 56T359 21T315 -3T256 -12Q150 -12 96 81T41 344Q41 522 105 616T302 710Q354 710 393 696T461 657T508 598T538 523L418 489Q411 511 403 532T381 568T348 593T301 603Q236 603 208 551T180 409V297Q180
|
||||
252 186 215T208 150T247 109T307 94Q363 94 392 127T421 213V270H294V365H538V0H421V97Z" />
|
||||
<glyph unicode="H" glyph-name="H" d="M402 300H198V0H67V698H198V408H402V698H533V0H402V300Z" />
|
||||
<glyph unicode="I" glyph-name="I" d="M78 0V98H235V600H78V698H522V600H365V98H522V0H78Z" />
|
||||
<glyph unicode="J" glyph-name="J" d="M502 698V191Q502 144 486 107T441 43T370 3T277 -12Q178 -12 122 38T49 171L172 197Q181 153 204 125T277 96Q322 96 346 122T370 206V590H134V698H502Z" />
|
||||
<glyph unicode="K" glyph-name="K" d="M277 306L204 210V0H73V698H204V384H210L291 500L438 698H586L367 404L596 0H448L277 306Z" />
|
||||
<glyph unicode="L" glyph-name="L" d="M100 0V698H231V108H540V0H100Z" />
|
||||
<glyph unicode="M" glyph-name="M" d="M438 323L444 525H435L300 166L165 525H156L162 323V0H48V698H204L301 441H308L406 698H552V0H438V323Z" />
|
||||
<glyph unicode="N" glyph-name="N" d="M189 501H179V0H67V698H228L411 197H421V698H533V0H372L189 501Z" />
|
||||
<glyph unicode="O" glyph-name="O" d="M300 -12Q232 -12 182 12T100 83T52 196T36 349Q36 434 51 501T99 615T182 685T300 710Q436 710 500 615T564 349Q564 178 500 83T300 -12ZM300 95Q335 95 359 108T397 147T418 208T425 290V409Q425 499 398 551T300 603Q230
|
||||
603 203 551T175 409V289Q175 199 202 147T300 95Z" />
|
||||
<glyph unicode="P" glyph-name="P" d="M80 0V698H345Q447 698 501 640T555 482Q555 382 501 324T345 266H211V0H80ZM211 373H318Q371 373 394 394T417 463V501Q417 548 394 569T318 591H211V373Z" />
|
||||
<glyph unicode="Q" glyph-name="Q" d="M516 -183H381Q307 -183 275 -146T243 -46V-7Q136 12 86 104T36 349Q36 434 51 501T99 615T182 685T300 710Q436 710 500 615T564 349Q564 196 514 104T357 -7V-88H516V-183ZM300 95Q335 95 359 108T397 147T418 208T425
|
||||
290V409Q425 499 398 551T300 603Q230 603 203 551T175 409V289Q175 199 202 147T300 95Z" />
|
||||
<glyph unicode="R" glyph-name="R" d="M211 0H80V698H345Q447 698 501 640T555 482Q555 405 520 353T418 286L567 0H423L290 271H211V0ZM318 373Q371 373 394 394T417 463V501Q417 548 394 569T318 591H211V373H318Z" />
|
||||
<glyph unicode="S" glyph-name="S" d="M292 -12Q202 -12 139 19T35 101L114 183Q153 138 198 117T296 95Q356 95 388 122T420 200Q420 242 396 263T315 294L241 306Q144 323 103 376T62 503Q62 603 127 656T307 710Q389 710 448 684T544 613L467 531Q439 564 400
|
||||
583T308 603Q194 603 194 509Q194 469 218 448T300 417L373 404Q464 387 508 337T552 209Q552 160 535 120T485 50T404 4T292 -12Z" />
|
||||
<glyph unicode="T" glyph-name="T" d="M365 590V0H235V590H25V698H575V590H365Z" />
|
||||
<glyph unicode="U" glyph-name="U" d="M193 698V263Q193 224 195 193T208 141T240 107T300 95Q338 95 359 107T391 140T404 193T407 263V698H538V283Q538 209 529 154T494 62T422 7T300 -12Q225 -12 179 6T106 62T71 154T62 283V698H193Z" />
|
||||
<glyph unicode="V" glyph-name="V" d="M208 0L17 698H161L248 345L299 125H306L358 345L446 698H583L392 0H208Z" />
|
||||
<glyph unicode="W" glyph-name="W" d="M65 0L18 698H131L153 296L162 130H171L241 537H363L433 130H442L451 296L473 698H582L535 0H366L304 395H296L234 0H65Z" />
|
||||
<glyph unicode="X" glyph-name="X" d="M586 0H438L362 139L301 261H293L230 139L152 0H14L219 360L27 698H176L240 580L301 459H308L370 580L437 698H575L382 358L586 0Z" />
|
||||
<glyph unicode="Y" glyph-name="Y" d="M235 0V255L8 698H154L235 530L298 387H305L369 530L451 698H592L365 255V0H235Z" />
|
||||
<glyph unicode="Z" glyph-name="Z" d="M552 0H48V115L395 590H60V698H539V583L193 108H552V0Z" />
|
||||
<glyph unicode="[" glyph-name="bracketleft" d="M195 -138V760H506V672H299V-50H506V-138H195Z" />
|
||||
<glyph unicode="\" glyph-name="backslash" d="M407 -138L78 760H193L522 -138H407Z" />
|
||||
<glyph unicode="]" glyph-name="bracketright" d="M405 760V-138H94V-50H301V672H94V760H405Z" />
|
||||
<glyph unicode="^" glyph-name="asciicircum" d="M466 253L302 582H293L129 253L32 300L232 698H368L568 300L466 253Z" />
|
||||
<glyph unicode="_" glyph-name="underscore" d="M60 -185V-78H540V-185H60Z" />
|
||||
<glyph unicode="`" glyph-name="grave" d="M161 745L269 799L364 612L287 575L161 745Z" />
|
||||
<glyph unicode="a" glyph-name="a" d="M490 0Q443 0 418 23T387 89H382Q368 41 327 15T226 -12Q148 -12 102 29T56 143Q56 299 285 299H376V333Q376 382 352 407T274 432Q225 432 195 413T144 364L71 426Q95 469 148 498T287 528Q389 528 446 481T504 339V96H565V0H490ZM269
|
||||
76Q315 76 345 97T376 156V225H288Q183 225 183 159V139Q183 108 206 92T269 76Z" />
|
||||
<glyph unicode="b" glyph-name="b" d="M69 740H197V425H204Q225 472 261 500T358 528Q402 528 438 512T501 462T541 377T555 258Q555 190 541 139T501 55T439 5T358 -12Q298 -12 262 16T204 91H197V0H69V740ZM304 90Q360 90 390 124T421 217V299Q421 358 391 392T304
|
||||
426Q260 426 229 405T197 339V177Q197 133 228 112T304 90Z" />
|
||||
<glyph unicode="c" glyph-name="c" d="M320 -12Q261 -12 214 7T135 61T85 146T67 258Q67 320 84 370T134 455T213 509T319 528Q401 528 451 494T527 404L428 350Q414 383 389 403T319 424Q262 424 232 391T201 301V215Q201 159 231 126T321 92Q367 92 394 113T439
|
||||
169L535 112Q509 57 457 23T320 -12Z" />
|
||||
<glyph unicode="d" glyph-name="d" d="M403 91H396Q375 44 339 16T242 -12Q198 -12 162 4T99 54T59 139T45 258Q45 394 99 461T242 528Q302 528 338 500T396 425H403V740H531V0H403V91ZM296 90Q318 90 337 95T371 111T394 138T403 177V339Q403 361 395 377T372
|
||||
404T338 420T296 426Q240 426 210 392T179 299V217Q179 158 209 124T296 90Z" />
|
||||
<glyph unicode="e" glyph-name="e" d="M312 -12Q250 -12 202 7T122 61T72 145T55 257Q55 320 72 370T122 455T199 509T302 528Q358 528 403 510T479 457T528 375T545 269V227H183V214Q183 158 218 124T316 89Q364 89 398 108T456 160L529 87Q501 46 448 17T312
|
||||
-12ZM303 434Q249 434 216 400T183 310V303H417V312Q417 368 387 401T303 434Z" />
|
||||
<glyph unicode="f" glyph-name="f" d="M75 101H233V415H65V516H233V600Q233 631 241 656T268 700T313 729T378 740H552V639H361V516H552V415H361V101H529V0H75V101Z" />
|
||||
<glyph unicode="g" glyph-name="g" d="M576 -54Q576 -135 509 -173T298 -212Q228 -212 181 -204T106 -179T65 -141T53 -90Q53 -47 78 -24T148 10V18Q118 28 101 46T83 96Q83 134 108 153T176 182V187Q130 209 104 249T78 346Q78 389 93 422T137 479T206 515T295
|
||||
528Q347 528 389 514V536Q389 566 408 586T463 606H556V510H434V493Q472 470 492 434T513 346Q513 303 498 269T454 212T385 177T295 164Q275 164 257 165T221 172Q207 167 194 156T181 127Q181 103 204 98T259 92H376Q430 92 468 82T530 52T565 5T576 -54ZM456
|
||||
-63Q456 -40 438 -27T369 -13H191Q161 -30 161 -63Q161 -90 183 -108T260 -126H343Q456 -126 456 -63ZM295 248Q344 248 367 271T390 333V359Q390 397 367 420T295 444Q247 444 224 421T201 359V333Q201 295 224 272T295 248Z" />
|
||||
<glyph unicode="h" glyph-name="h" d="M75 740H203V425H208Q225 467 260 497T359 528Q435 528 482 478T530 333V0H402V315Q402 427 305 427Q285 427 267 422T235 407T212 381T203 345V0H75V740Z" />
|
||||
<glyph unicode="i" glyph-name="i" d="M332 596Q287 596 269 615T250 661V685Q250 712 268 731T332 750Q377 750 395 731T414 685V661Q414 634 396 615T332 596ZM98 101H268V415H98V516H396V101H554V0H98V101Z" />
|
||||
<glyph unicode="j" glyph-name="j" d="M95 -99H308V415H91V516H436V-60Q436 -122 401 -161T292 -200H95V-99ZM373 596Q328 596 310 615T291 661V685Q291 712 309 731T373 750Q418 750 436 731T455 685V661Q455 634 437 615T373 596Z" />
|
||||
<glyph unicode="k" glyph-name="k" d="M77 740H205V306H211L286 388L413 516H562L357 313L584 0H431L266 240L205 182V0H77V740Z" />
|
||||
<glyph unicode="l" glyph-name="l" d="M72 101H236V639H72V740H364V101H529V0H72V101Z" />
|
||||
<glyph unicode="m" glyph-name="m" d="M37 0V516H143V445H149Q161 479 183 503T247 528Q286 528 309 506T339 442H344Q357 477 382 502T455 528Q512 528 537 487T562 366V0H457V351Q457 396 445 414T407 432Q384 432 369 417T353 371V0H247V351Q247 396 235 414T198
|
||||
432Q174 432 159 417T143 371V0H37Z" />
|
||||
<glyph unicode="n" glyph-name="n" d="M75 0V516H203V425H208Q225 467 260 497T359 528Q435 528 482 478T530 333V0H402V315Q402 427 305 427Q285 427 267 422T235 407T212 381T203 345V0H75Z" />
|
||||
<glyph unicode="o" glyph-name="o" d="M300 -12Q241 -12 195 7T116 61T66 146T48 258Q48 320 65 370T115 455T194 509T300 528Q358 528 405 509T484 455T534 370T552 258Q552 196 535 146T485 61T405 7T300 -12ZM300 86Q355 86 387 119T419 218V298Q419 363 387
|
||||
396T300 430Q245 430 213 397T181 298V218Q181 153 213 120T300 86Z" />
|
||||
<glyph unicode="p" glyph-name="p" d="M69 516H197V425H204Q225 472 261 500T358 528Q402 528 438 512T501 462T541 377T555 258Q555 190 541 139T501 55T439 5T358 -12Q298 -12 262 16T204 91H197V-200H69V516ZM304 90Q360 90 390 124T421 217V299Q421 358 391
|
||||
392T304 426Q260 426 229 405T197 339V177Q197 133 228 112T304 90Z" />
|
||||
<glyph unicode="q" glyph-name="q" d="M403 91H396Q375 44 339 16T242 -12Q198 -12 162 4T99 54T59 139T45 258Q45 394 99 461T242 528Q302 528 338 500T396 425H403V516H531V-200H403V91ZM296 90Q318 90 337 95T371 111T394 138T403 177V339Q403 361 395 377T372
|
||||
404T338 420T296 426Q240 426 210 392T179 299V217Q179 158 209 124T296 90Z" />
|
||||
<glyph unicode="r" glyph-name="r" d="M75 101H203V415H75V516H331V375H339Q346 402 358 427T390 472T438 504T505 516H561V396H455Q397 396 364 362T331 275V101H510V0H75V101Z" />
|
||||
<glyph unicode="s" glyph-name="s" d="M298 -12Q213 -12 151 14T54 86L129 154Q161 119 202 100T299 81Q347 81 376 97T405 146Q405 174 386 184T334 199L251 212Q219 217 189 226T135 253T97 296T83 361Q83 442 143 485T310 528Q385 528 437 507T524 448L457
|
||||
374Q435 398 399 416T306 435Q208 435 208 374Q208 346 228 336T280 320L362 307Q395 302 425 293T479 266T517 223T531 159Q531 79 470 34T298 -12Z" />
|
||||
<glyph unicode="t" glyph-name="t" d="M335 0Q261 0 226 39T191 140V415H41V516H143Q174 516 187 528T200 573V698H319V516H529V415H319V101H529V0H335Z" />
|
||||
<glyph unicode="u" glyph-name="u" d="M397 91H392Q375 49 340 19T241 -12Q165 -12 118 38T70 183V516H198V201Q198 89 295 89Q314 89 332 94T365 109T388 135T397 171V516H525V0H397V91Z" />
|
||||
<glyph unicode="v" glyph-name="v" d="M223 0L38 516H172L241 297L299 107H306L364 297L433 516H562L377 0H223Z" />
|
||||
<glyph unicode="w" glyph-name="w" d="M14 516H121L165 90H174L244 516H361L430 90H439L484 516H586L520 0H365L305 379H296L235 0H80L14 516Z" />
|
||||
<glyph unicode="x" glyph-name="x" d="M34 0L227 262L45 516H192L254 425L302 353H309L357 425L419 516H555L373 268L566 0H418L347 104L298 177H291L242 104L171 0H34Z" />
|
||||
<glyph unicode="y" glyph-name="y" d="M441 516H571L324 -92Q303 -145 268 -172T169 -200H75V-99H198L237 5L29 516H165L244 300L299 136H306L361 300L441 516Z" />
|
||||
<glyph unicode="z" glyph-name="z" d="M72 0V109L372 415H82V516H518V407L218 101H528V0H72Z" />
|
||||
<glyph unicode="{" glyph-name="braceleft" d="M327 -138Q267 -138 240 -110T213 -38V12Q213 38 218 57T232 92T250 119T269 142Q286 162 292 176T299 207Q299 267 183 267H96V355H183Q299 355 299 415Q299 431 293 445T269 480Q260 491 250 503T232 530T219 564T213
|
||||
610V660Q213 704 240 732T327 760H504V672H317V616Q317 577 329 556T357 515Q374 494 389 470T404 414Q404 372 375 347T289 314V308Q346 301 375 276T404 208Q404 176 389 152T357 107Q341 87 329 66T317 6V-50H504V-138H327Z" />
|
||||
<glyph unicode="|" glyph-name="bar" d="M245 -138V760H355V-138H245Z" />
|
||||
<glyph unicode="}" glyph-name="braceright" d="M273 760Q333 760 360 732T387 660V610Q387 584 382 565T368 530T350 503T331 480Q314 460 308 446T301 415Q301 355 417 355H504V267H417Q301 267 301 207Q301 191 307 177T331 142Q340 131 350 119T368 92T381
|
||||
58T387 12V-38Q387 -82 360 -110T273 -138H96V-50H283V6Q283 45 271 66T243 107Q226 128 211 152T196 208Q196 250 225 275T311 308V314Q254 321 225 346T196 414Q196 446 211 470T243 515Q259 535 271 556T283 616V672H96V760H273Z" />
|
||||
<glyph unicode="~" glyph-name="asciitilde" d="M409 218Q380 218 352 227T291 253Q265 266 244 276T202 286Q176 286 161 268T133 218L41 252Q56 314 94 353T191 392Q220 392 248 383T309 357Q335 344 356 334T398 324Q424 324 439 342T467 392L559 358Q544 296
|
||||
506 257T409 218Z" />
|
||||
<glyph unicode=" " glyph-name="uni00A0" />
|
||||
<glyph unicode="¡" glyph-name="exclamdown" d="M230 -182V21L279 276H321L370 21V-182H230ZM300 355Q250 355 230 376T209 427V455Q209 485 229 506T300 527Q350 527 370 506T391 455V427Q391 397 371 376T300 355Z" />
|
||||
<glyph unicode="¢" glyph-name="cent" d="M264 -114V-7Q170 9 119 79T67 258Q67 367 118 437T264 523V630H363V525Q426 517 465 485T527 404L433 353Q422 378 403 397T356 425V90Q387 98 408 118T444 165L535 112Q512 64 470 32T363 -9V-114H264ZM192 215Q192
|
||||
170 212 138T272 93V423Q233 411 213 379T192 301V215Z" />
|
||||
<glyph unicode="£" glyph-name="sterling" d="M71 0V127Q154 162 154 251Q154 257 154 263T152 274H48V372H121Q109 401 100 432T90 502Q90 548 107 586T157 652T234 695T334 710Q412 710 465 682T556 602L468 524Q443 557 413 577T336 597Q284 597 254 569T223
|
||||
484Q223 452 231 425T251 372H434V274H281Q283 266 283 257Q283 230 276 209T257 170T231 141T203 122V114H548V0H71Z" />
|
||||
<glyph unicode="¥" glyph-name="yen" d="M62 100H238V231H62V331H176L7 698H144L298 331H306L460 698H593L424 331H538V231H362V100H538V0H62V100Z" />
|
||||
<glyph unicode="¦" glyph-name="brokenbar" d="M245 401V760H355V401H245ZM245 -138V221H355V-138H245Z" />
|
||||
<glyph unicode="§" glyph-name="section" d="M504 3Q504 -36 488 -68T444 -122T376 -157T288 -170Q238 -170 188 -155T96 -102L162 -24Q187 -49 219 -62T289 -76Q337 -76 364 -56T392 -1Q392 29 369 46T299 76L225 97Q182 109 153 125T105 160T78 202T70
|
||||
248Q70 293 97 328T180 387V400Q139 426 121 460T102 537Q102 577 117 609T161 663T230 698T318 710Q368 710 418 695T510 643L444 564Q418 589 387 602T317 616Q269 616 242 596T214 542Q214 512 237 494T307 464L381 443Q424 431 453 415T501 380T528 338T536
|
||||
292Q536 247 509 212T426 153V140Q467 114 485 80T504 3ZM426 255Q426 286 406 309T332 346L260 366Q251 368 243 371T227 377Q204 357 192 335T180 285Q180 254 200 231T274 194L346 174Q355 172 363 169T379 163Q402 183 414 205T426 255Z" />
|
||||
<glyph unicode="¨" glyph-name="dieresis" d="M195 599Q154 599 138 616T122 657V679Q122 703 138 720T195 737Q236 737 252 720T268 679V657Q268 633 252 616T195 599ZM405 599Q364 599 348 616T332 657V679Q332 703 348 720T405 737Q446 737 462 720T478
|
||||
679V657Q478 633 462 616T405 599Z" />
|
||||
<glyph unicode="©" glyph-name="copyright" d="M300 18Q237 18 183 41T88 107T24 211T0 349Q0 425 23 486T87 591T182 657T300 680Q363 680 417 657T512 591T576 487T600 349Q600 272 577 211T513 107T418 41T300 18ZM300 73Q351 73 394 92T469 144T519 223T537
|
||||
319V379Q537 430 519 475T470 553T395 606T300 625Q249 625 206 606T131 554T81 475T63 379V319Q63 268 81 223T130 145T205 92T300 73ZM308 158Q227 158 180 210T133 349Q133 392 145 427T181 488T237 526T308 540Q365 540 400 513T453 444L378 405Q367 429 351
|
||||
443T309 457Q277 457 259 437T240 383V315Q240 282 258 262T310 241Q339 241 356 255T385 293L459 254Q442 215 406 187T308 158Z" />
|
||||
<glyph unicode="ª" glyph-name="ordfeminine" d="M429 345Q364 345 358 406H354Q344 374 316 356T244 337Q190 337 160 365T129 443Q129 498 169 525T288 552H350V572Q350 607 334 624T282 641Q248 641 227 628T192 594L140 640Q157 669 194 689T291 710Q362
|
||||
710 402 676T442 576V414H481V345H429ZM289 498Q219 498 219 454V441Q219 421 234 411T275 400Q306 400 328 414T350 456V498H289Z" />
|
||||
<glyph unicode="«" glyph-name="guillemotleft" d="M523 43L321 213V329L523 499L562 415L427 271L562 127L523 43ZM507 43L305 213V329L507 499L546 415L411 271L546 127L507 43Z" />
|
||||
<glyph unicode="¬" glyph-name="logicalnot" d="M416 61V254H62V357H522V61H416Z" />
|
||||
<glyph unicode="­" glyph-name="uni00AD" d="M145 242V367H455V242H145Z" />
|
||||
<glyph unicode="®" glyph-name="registered" d="M300 346Q262 346 229 360T170 398T131 455T117 528Q117 567 131 600T170 658T228 696T300 710Q338 710 371 696T429 658T468 601T483 528Q483 489 469 456T430 398T372 360T300 346ZM300 387Q329 387 353
|
||||
396T396 423T424 464T435 518V538Q435 568 425 592T396 633T354 659T300 669Q271 669 247 660T204 633T176 592T165 538V518Q165 488 175 464T204 423T246 397T300 387ZM275 436H225V621H315Q349 621 365 604T382 560Q382 520 347 504L388 436H333L299 495H275V436ZM303
|
||||
532Q329 532 329 553V561Q329 582 303 582H275V532H303Z" />
|
||||
<glyph unicode="¯" glyph-name="overscore" d="M151 714H449V622H151V714Z" />
|
||||
<glyph unicode="°" glyph-name="degree" d="M300 344Q261 344 228 358T170 396T131 454T117 527Q117 566 131 599T170 657T228 696T300 710Q339 710 372 696T430 658T469 600T483 527Q483 488 469 455T430 397T372 358T300 344ZM300 430Q342 430 366 457T391
|
||||
527Q391 569 367 596T300 624Q258 624 234 597T209 527Q209 485 233 458T300 430Z" />
|
||||
<glyph unicode="±" glyph-name="plusminus" d="M244 174V350H62V453H244V629H356V453H538V350H356V174H244ZM62 0V104H538V0H62Z" />
|
||||
<glyph unicode="²" glyph-name="twosuperior" d="M446 329H163V408L278 497Q313 524 328 542T343 585V587Q343 610 328 619T293 628Q266 628 252 614T232 579L153 609Q167 648 203 676T302 704Q368 704 404 673T440 592Q440 568 431 548T407 510T373 477T331
|
||||
448L261 402H446V329Z" />
|
||||
<glyph unicode="³" glyph-name="threesuperior" d="M284 554Q315 554 328 565T342 592V596Q342 613 329 623T292 634Q268 634 247 623T210 590L154 642Q178 671 210 687T294 704Q360 704 399 677T438 605Q438 570 416 549T361 523V519Q398 513 421 491T444
|
||||
432Q444 382 402 353T291 323Q260 323 237 329T196 344T165 367T142 395L209 446Q221 423 240 408T291 393Q319 393 334 405T349 437V440Q349 463 332 472T284 482H248V554H284Z" />
|
||||
<glyph unicode="´" glyph-name="acute" d="M313 575L236 612L331 799L439 745L313 575Z" />
|
||||
<glyph unicode="µ" glyph-name="mu" d="M75 -200V516H203V208Q203 89 297 89Q316 89 334 94T366 109T388 135T397 171V516H525V0H397V91H392Q385 71 375 52T349 19T314 -3T268 -12Q242 -12 222 -2T185 33H180L203 -86V-200H75Z" />
|
||||
<glyph unicode="¶" glyph-name="paragraph" d="M253 246Q206 246 166 263T94 310T46 381T28 472Q28 521 45 562T94 634T165 681T253 698H538V-149H432V597H359V-149H253V246Z" />
|
||||
<glyph unicode="·" glyph-name="middot" d="M300 213Q248 213 226 235T204 289V319Q204 351 226 373T300 395Q352 395 374 373T396 319V289Q396 257 374 235T300 213Z" />
|
||||
<glyph unicode="¸" glyph-name="cedilla" d="M313 -210Q265 -210 237 -196T197 -167L241 -116Q250 -128 266 -136T305 -145Q324 -145 336 -139T349 -118Q349 -106 335 -95T276 -78L253 -75L274 27H336L318 -57L322 -61Q333 -58 345 -56T368 -54Q397 -54 418
|
||||
-70T439 -122Q439 -146 429 -163T401 -190T361 -205T313 -210Z" />
|
||||
<glyph unicode="¹" glyph-name="onesuperior" d="M184 329V399H282V617L195 576L161 639L277 698H372V399H458V329H184Z" />
|
||||
<glyph unicode="º" glyph-name="ordmasculine" d="M300 338Q218 338 172 387T125 524Q125 611 171 660T300 710Q382 710 428 661T475 524Q475 437 429 388T300 338ZM300 408Q338 408 358 431T379 495V553Q379 594 359 617T300 640Q262 640 242 617T221 553V495Q221
|
||||
454 241 431T300 408Z" />
|
||||
<glyph unicode="»" glyph-name="guillemotright" d="M55 127L190 271L55 415L94 499L296 329V213L94 43L55 127ZM295 127L430 271L295 415L334 499L536 329V213L334 43L295 127Z" />
|
||||
<glyph unicode="¼" glyph-name="onequarter" d="M38 426H126V631L45 590L16 644L120 698H206V426H283V363H38V426ZM500 698H594L425 398H331L500 698ZM175 300H269L100 0H6L175 300ZM467 64H312V133L446 335H543V120H588V64H543V0H467V64ZM467 120V260H462L369
|
||||
120H467Z" />
|
||||
<glyph unicode="½" glyph-name="onehalf" d="M38 426H126V631L45 590L16 644L120 698H206V426H283V363H38V426ZM500 698H594L425 398H331L500 698ZM175 300H269L100 0H6L175 300ZM324 70L432 153Q464 178 476 196T488 237V240Q488 257 476 266T443 276Q419
|
||||
276 405 263T382 227L315 253Q321 270 331 286T357 314T395 333T448 341Q508 341 540 312T573 241Q573 220 566 203T545 169T513 138T472 107L412 63H580V0H324V70Z" />
|
||||
<glyph unicode="¾" glyph-name="threequarters" d="M147 357Q92 357 61 378T13 425L71 464Q82 444 99 431T146 418Q198 418 198 458V465Q198 485 181 495T136 506H106V564H138Q167 564 180 574T194 601V609Q194 623 182 633T147 643Q124 643 105 633T71 602L22
|
||||
646Q44 673 72 688T147 704Q207 704 242 680T277 614Q277 582 257 563T207 538V534Q241 529 262 510T283 456Q283 411 246 384T147 357ZM500 698H594L425 398H331L500 698ZM175 300H269L100 0H6L175 300ZM467 64H312V133L446 335H543V120H588V64H543V0H467V64ZM467
|
||||
120V260H462L369 120H467Z" />
|
||||
<glyph unicode="¿" glyph-name="questiondown" d="M306 -194Q255 -194 213 -180T141 -139T95 -76T78 6Q78 50 93 84T133 144T192 186T261 208V288H383V134Q305 131 260 105T214 18V4Q214 -41 241 -62T310 -84Q357 -84 386 -58T426 11L534 -33Q525 -64 507
|
||||
-93T462 -144T396 -180T306 -194ZM319 355Q269 355 249 376T228 427V455Q228 485 248 506T319 527Q369 527 389 506T410 455V427Q410 397 390 376T319 355Z" />
|
||||
<glyph unicode="À" glyph-name="Agrave" d="M449 0L400 177H193L145 0H13L209 698H391L588 0H449ZM301 569H293L216 284H378L301 569ZM161 915L269 969L364 782L287 745L161 915Z" />
|
||||
<glyph unicode="Á" glyph-name="Aacute" d="M449 0L400 177H193L145 0H13L209 698H391L588 0H449ZM301 569H293L216 284H378L301 569ZM313 745L236 782L331 969L439 915L313 745Z" />
|
||||
<glyph unicode="Â" glyph-name="Acircumflex" d="M449 0L400 177H193L145 0H13L209 698H391L588 0H449ZM301 569H293L216 284H378L301 569ZM358 950L484 795L418 746L298 861L178 746L116 795L242 950H358Z" />
|
||||
<glyph unicode="Ã" glyph-name="Atilde" d="M449 0L400 177H193L145 0H13L209 698H391L588 0H449ZM301 569H293L216 284H378L301 569ZM375 768Q351 768 333 775T297 790Q275 800 257 806T222 812Q205 812 192 806T163 787L117 846Q134 872 160 889T225 907Q249
|
||||
907 267 900T303 885Q325 875 343 869T378 863Q395 863 408 869T437 888L483 829Q466 803 440 786T375 768Z" />
|
||||
<glyph unicode="Ä" glyph-name="Adieresis" d="M449 0L400 177H193L145 0H13L209 698H391L588 0H449ZM301 569H293L216 284H378L301 569ZM195 769Q154 769 138 786T122 827V849Q122 873 138 890T195 907Q236 907 252 890T268 849V827Q268 803 252 786T195
|
||||
769ZM405 769Q364 769 348 786T332 827V849Q332 873 348 890T405 907Q446 907 462 890T478 849V827Q478 803 462 786T405 769Z" />
|
||||
<glyph unicode="Å" glyph-name="Aring" d="M449 0L400 177H193L145 0H13L209 698H391L588 0H449ZM301 569H293L216 284H378L301 569ZM300 726Q271 726 246 736T202 764T173 806T162 861Q162 890 172 915T201 958T245 986T300 996Q329 996 354 986T398 958T427
|
||||
916T438 861Q438 832 428 807T399 764T355 736T300 726ZM300 793Q326 793 339 807T353 845V877Q353 929 300 929Q274 929 261 915T247 877V845Q247 793 300 793Z" />
|
||||
<glyph unicode="Æ" glyph-name="AE" d="M289 178H170L131 0H3L176 698H568V599H410V403H558V304H410V99H568V0H289V178ZM264 607L191 276H289V607H264Z" />
|
||||
<glyph unicode="Ç" glyph-name="Ccedilla" d="M316 603Q248 603 219 551T189 409V289Q189 200 218 148T316 95Q344 95 364 103T397 127T420 164T436 209L556 175Q545 135 527 102T481 44T418 5T332 -12L323 -57L327 -61Q338 -58 349 -56T372 -54Q402 -54
|
||||
423 -70T444 -122Q444 -146 434 -163T406 -190T365 -205T318 -210Q269 -210 242 -196T201 -167L245 -116Q255 -128 271 -136T310 -145Q329 -145 341 -139T354 -118Q354 -106 340 -95T281 -78L258 -75L271 -9Q158 7 104 99T50 349Q50 521 115 615T318 710Q370 710
|
||||
409 696T478 658T526 599T556 523L436 489Q429 513 420 534T398 570T364 594T316 603Z" />
|
||||
<glyph unicode="È" glyph-name="Egrave" d="M164 915L272 969L367 782L290 745L164 915ZM83 0V698H524V590H214V408H513V300H214V108H524V0H83Z" />
|
||||
<glyph unicode="É" glyph-name="Eacute" d="M83 0V698H524V590H214V408H513V300H214V108H524V0H83ZM316 745L239 782L334 969L442 915L316 745Z" />
|
||||
<glyph unicode="Ê" glyph-name="Ecircumflex" d="M361 950L487 795L421 746L301 861L181 746L119 795L245 950H361ZM83 0V698H524V590H214V408H513V300H214V108H524V0H83Z" />
|
||||
<glyph unicode="Ë" glyph-name="Edieresis" d="M83 0V698H524V590H214V408H513V300H214V108H524V0H83ZM198 769Q157 769 141 786T125 827V849Q125 873 141 890T198 907Q239 907 255 890T271 849V827Q271 803 255 786T198 769ZM408 769Q367 769 351 786T335
|
||||
827V849Q335 873 351 890T408 907Q449 907 465 890T481 849V827Q481 803 465 786T408 769Z" />
|
||||
<glyph unicode="Ì" glyph-name="Igrave" d="M78 0V98H235V600H78V698H522V600H365V98H522V0H78ZM161 915L269 969L364 782L287 745L161 915Z" />
|
||||
<glyph unicode="Í" glyph-name="Iacute" d="M78 0V98H235V600H78V698H522V600H365V98H522V0H78ZM313 745L236 782L331 969L439 915L313 745Z" />
|
||||
<glyph unicode="Î" glyph-name="Icircumflex" d="M78 0V98H235V600H78V698H522V600H365V98H522V0H78ZM358 950L484 795L418 746L298 861L178 746L116 795L242 950H358Z" />
|
||||
<glyph unicode="Ï" glyph-name="Idieresis" d="M78 0V98H235V600H78V698H522V600H365V98H522V0H78ZM195 769Q154 769 138 786T122 827V849Q122 873 138 890T195 907Q236 907 252 890T268 849V827Q268 803 252 786T195 769ZM405 769Q364 769 348 786T332 827V849Q332
|
||||
873 348 890T405 907Q446 907 462 890T478 849V827Q478 803 462 786T405 769Z" />
|
||||
<glyph unicode="Ð" glyph-name="Eth" d="M79 319H14V418H79V698H290Q426 698 492 608T559 349Q559 179 493 90T290 0H79V319ZM281 105Q353 105 387 152T422 289V410Q422 499 388 546T281 593H208V414H327V322H208V105H281Z" />
|
||||
<glyph unicode="Ñ" glyph-name="Ntilde" d="M375 768Q351 768 333 775T297 790Q275 800 257 806T222 812Q205 812 192 806T163 787L117 846Q134 872 160 889T225 907Q249 907 267 900T303 885Q325 875 343 869T378 863Q395 863 408 869T437 888L483 829Q466
|
||||
803 440 786T375 768ZM189 501H179V0H67V698H228L411 197H421V698H533V0H372L189 501Z" />
|
||||
<glyph unicode="Ò" glyph-name="Ograve" d="M161 915L269 969L364 782L287 745L161 915ZM300 -12Q232 -12 182 12T100 83T52 196T36 349Q36 434 51 501T99 615T182 685T300 710Q436 710 500 615T564 349Q564 178 500 83T300 -12ZM300 95Q335 95 359 108T397
|
||||
147T418 208T425 290V409Q425 499 398 551T300 603Q230 603 203 551T175 409V289Q175 199 202 147T300 95Z" />
|
||||
<glyph unicode="Ó" glyph-name="Oacute" d="M313 745L236 782L331 969L439 915L313 745ZM300 -12Q232 -12 182 12T100 83T52 196T36 349Q36 434 51 501T99 615T182 685T300 710Q436 710 500 615T564 349Q564 178 500 83T300 -12ZM300 95Q335 95 359 108T397
|
||||
147T418 208T425 290V409Q425 499 398 551T300 603Q230 603 203 551T175 409V289Q175 199 202 147T300 95Z" />
|
||||
<glyph unicode="Ô" glyph-name="Ocircumflex" d="M358 950L484 795L418 746L298 861L178 746L116 795L242 950H358ZM300 -12Q232 -12 182 12T100 83T52 196T36 349Q36 434 51 501T99 615T182 685T300 710Q436 710 500 615T564 349Q564 178 500 83T300 -12ZM300
|
||||
95Q335 95 359 108T397 147T418 208T425 290V409Q425 499 398 551T300 603Q230 603 203 551T175 409V289Q175 199 202 147T300 95Z" />
|
||||
<glyph unicode="Õ" glyph-name="Otilde" d="M300 -12Q232 -12 182 12T100 83T52 196T36 349Q36 434 51 501T99 615T182 685T300 710Q436 710 500 615T564 349Q564 178 500 83T300 -12ZM300 95Q335 95 359 108T397 147T418 208T425 290V409Q425 499 398 551T300
|
||||
603Q230 603 203 551T175 409V289Q175 199 202 147T300 95ZM375 768Q351 768 333 775T297 790Q275 800 257 806T222 812Q205 812 192 806T163 787L117 846Q134 872 160 889T225 907Q249 907 267 900T303 885Q325 875 343 869T378 863Q395 863 408 869T437 888L483
|
||||
829Q466 803 440 786T375 768Z" />
|
||||
<glyph unicode="Ö" glyph-name="Odieresis" d="M300 -12Q232 -12 182 12T100 83T52 196T36 349Q36 434 51 501T99 615T182 685T300 710Q436 710 500 615T564 349Q564 178 500 83T300 -12ZM300 95Q335 95 359 108T397 147T418 208T425 290V409Q425 499 398
|
||||
551T300 603Q230 603 203 551T175 409V289Q175 199 202 147T300 95ZM195 769Q154 769 138 786T122 827V849Q122 873 138 890T195 907Q236 907 252 890T268 849V827Q268 803 252 786T195 769ZM405 769Q364 769 348 786T332 827V849Q332 873 348 890T405 907Q446
|
||||
907 462 890T478 849V827Q478 803 462 786T405 769Z" />
|
||||
<glyph unicode="×" glyph-name="multiply" d="M300 231L150 80L75 155L226 305L75 455L150 530L300 379L450 530L525 455L374 305L525 155L450 80L300 231Z" />
|
||||
<glyph unicode="Ø" glyph-name="Oslash" d="M300 -12Q217 -12 162 24L111 -59L27 -11L92 94Q63 140 50 204T36 349Q36 434 51 501T99 615T182 685T300 710Q382 710 438 674L489 757L573 709L508 604Q537 558 550 494T564 349Q564 178 500 83T300 -12ZM175
|
||||
289Q175 272 176 256T180 225L382 573Q352 603 300 603Q230 603 203 551T175 409V289ZM300 95Q370 95 397 147T425 289V409Q425 426 424 442T420 473L218 125Q248 95 300 95Z" />
|
||||
<glyph unicode="Ù" glyph-name="Ugrave" d="M161 915L269 969L364 782L287 745L161 915ZM193 698V263Q193 224 195 193T208 141T240 107T300 95Q338 95 359 107T391 140T404 193T407 263V698H538V283Q538 209 529 154T494 62T422 7T300 -12Q225 -12 179 6T106
|
||||
62T71 154T62 283V698H193Z" />
|
||||
<glyph unicode="Ú" glyph-name="Uacute" d="M193 698V263Q193 224 195 193T208 141T240 107T300 95Q338 95 359 107T391 140T404 193T407 263V698H538V283Q538 209 529 154T494 62T422 7T300 -12Q225 -12 179 6T106 62T71 154T62 283V698H193ZM313 745L236
|
||||
782L331 969L439 915L313 745Z" />
|
||||
<glyph unicode="Û" glyph-name="Ucircumflex" d="M358 950L484 795L418 746L298 861L178 746L116 795L242 950H358ZM193 698V263Q193 224 195 193T208 141T240 107T300 95Q338 95 359 107T391 140T404 193T407 263V698H538V283Q538 209 529 154T494 62T422
|
||||
7T300 -12Q225 -12 179 6T106 62T71 154T62 283V698H193Z" />
|
||||
<glyph unicode="Ü" glyph-name="Udieresis" d="M193 698V263Q193 224 195 193T208 141T240 107T300 95Q338 95 359 107T391 140T404 193T407 263V698H538V283Q538 209 529 154T494 62T422 7T300 -12Q225 -12 179 6T106 62T71 154T62 283V698H193ZM195 769Q154
|
||||
769 138 786T122 827V849Q122 873 138 890T195 907Q236 907 252 890T268 849V827Q268 803 252 786T195 769ZM405 769Q364 769 348 786T332 827V849Q332 873 348 890T405 907Q446 907 462 890T478 849V827Q478 803 462 786T405 769Z" />
|
||||
<glyph unicode="Ý" glyph-name="Yacute" d="M235 0V255L8 698H154L235 530L298 387H305L369 530L451 698H592L365 255V0H235ZM313 745L236 782L331 969L439 915L313 745Z" />
|
||||
<glyph unicode="Þ" glyph-name="Thorn" d="M80 0V698H211V570H342Q393 570 432 555T497 511T537 443T551 354Q551 256 498 198T342 140H211V0H80ZM211 246H316Q369 246 392 267T415 336V373Q415 420 392 441T316 463H211V246Z" />
|
||||
<glyph unicode="ß" glyph-name="germandbls" d="M72 0V600Q72 631 80 656T107 700T152 729T217 740H369V642H197V516H535V418L387 212Q475 200 524 152T573 15Q573 -90 502 -145T311 -200H245V-102H305Q376 -102 409 -77T443 3V27Q443 81 408 105T295 129H274V227L412
|
||||
418H197V0H72Z" />
|
||||
<glyph unicode="à" glyph-name="agrave" d="M490 0Q443 0 418 23T387 89H382Q368 41 327 15T226 -12Q148 -12 102 29T56 143Q56 299 285 299H376V333Q376 382 352 407T274 432Q225 432 195 413T144 364L71 426Q95 469 148 498T287 528Q389 528 446 481T504
|
||||
339V96H565V0H490ZM269 76Q315 76 345 97T376 156V225H288Q183 225 183 159V139Q183 108 206 92T269 76ZM408 745L516 799L611 612L534 575L408 745Z" />
|
||||
<glyph unicode="á" glyph-name="aacute" d="M490 0Q443 0 418 23T387 89H382Q368 41 327 15T226 -12Q148 -12 102 29T56 143Q56 299 285 299H376V333Q376 382 352 407T274 432Q225 432 195 413T144 364L71 426Q95 469 148 498T287 528Q389 528 446 481T504
|
||||
339V96H565V0H490ZM269 76Q315 76 345 97T376 156V225H288Q183 225 183 159V139Q183 108 206 92T269 76ZM560 575L483 612L578 799L686 745L560 575Z" />
|
||||
<glyph unicode="â" glyph-name="acircumflex" d="M490 0Q443 0 418 23T387 89H382Q368 41 327 15T226 -12Q148 -12 102 29T56 143Q56 299 285 299H376V333Q376 382 352 407T274 432Q225 432 195 413T144 364L71 426Q95 469 148 498T287 528Q389 528 446 481T504
|
||||
339V96H565V0H490ZM269 76Q315 76 345 97T376 156V225H288Q183 225 183 159V139Q183 108 206 92T269 76ZM605 780L731 625L665 576L545 691L425 576L363 625L489 780H605Z" />
|
||||
<glyph unicode="ã" glyph-name="atilde" d="M490 0Q443 0 418 23T387 89H382Q368 41 327 15T226 -12Q148 -12 102 29T56 143Q56 299 285 299H376V333Q376 382 352 407T274 432Q225 432 195 413T144 364L71 426Q95 469 148 498T287 528Q389 528 446 481T504
|
||||
339V96H565V0H490ZM269 76Q315 76 345 97T376 156V225H288Q183 225 183 159V139Q183 108 206 92T269 76ZM622 598Q598 598 580 605T544 620Q522 630 504 636T469 642Q452 642 439 636T410 617L364 676Q381 702 407 719T472 737Q496 737 514 730T550 715Q572 705
|
||||
590 699T625 693Q642 693 655 699T684 718L730 659Q713 633 687 616T622 598Z" />
|
||||
<glyph unicode="ä" glyph-name="adieresis" d="M490 0Q443 0 418 23T387 89H382Q368 41 327 15T226 -12Q148 -12 102 29T56 143Q56 299 285 299H376V333Q376 382 352 407T274 432Q225 432 195 413T144 364L71 426Q95 469 148 498T287 528Q389 528 446 481T504
|
||||
339V96H565V0H490ZM269 76Q315 76 345 97T376 156V225H288Q183 225 183 159V139Q183 108 206 92T269 76ZM442 599Q401 599 385 616T369 657V679Q369 703 385 720T442 737Q483 737 499 720T515 679V657Q515 633 499 616T442 599ZM652 599Q611 599 595 616T579 657V679Q579
|
||||
703 595 720T652 737Q693 737 709 720T725 679V657Q725 633 709 616T652 599Z" />
|
||||
<glyph unicode="å" glyph-name="aring" d="M490 0Q443 0 418 23T387 89H382Q368 41 327 15T226 -12Q148 -12 102 29T56 143Q56 299 285 299H376V333Q376 382 352 407T274 432Q225 432 195 413T144 364L71 426Q95 469 148 498T287 528Q389 528 446 481T504
|
||||
339V96H565V0H490ZM269 76Q315 76 345 97T376 156V225H288Q183 225 183 159V139Q183 108 206 92T269 76ZM547 556Q518 556 493 566T449 594T420 636T409 691Q409 720 419 745T448 788T492 816T547 826Q576 826 601 816T645 788T674 746T685 691Q685 662 675 637T646
|
||||
594T602 566T547 556ZM547 623Q573 623 586 637T600 675V707Q600 759 547 759Q521 759 508 745T494 707V675Q494 623 547 623Z" />
|
||||
<glyph unicode="æ" glyph-name="ae" d="M138 -12Q82 -12 44 26T6 140Q6 220 52 261T187 302H233V358Q233 445 169 445Q138 445 123 426T96 374L16 407Q32 465 71 496T169 528Q215 528 246 509T294 454H298Q339 528 419 528Q496 528 539 467T583 296V231H339V213Q339
|
||||
151 359 113T418 75Q449 75 465 98T491 152L577 125Q569 98 556 73T524 29T478 -1T418 -12Q366 -12 325 16T265 104H261Q250 44 218 16T138 -12ZM165 70Q202 70 217 97T233 172V231H198Q156 231 133 213T109 156V138Q109 106 122 88T165 70ZM413 447Q376 447 358
|
||||
419T339 343V302H487V343Q487 391 469 419T413 447Z" />
|
||||
<glyph unicode="ç" glyph-name="ccedilla" d="M319 528Q401 528 451 494T527 404L428 350Q414 383 389 403T319 424Q262 424 232 391T201 301V215Q201 159 231 126T321 92Q367 92 394 113T439 169L535 112Q509 58 459 24T329 -12L320 -57L324 -61Q335 -58
|
||||
346 -56T369 -54Q399 -54 420 -70T441 -122Q441 -146 431 -163T403 -190T362 -205T315 -210Q267 -210 239 -196T199 -167L242 -116Q252 -128 268 -136T307 -145Q326 -145 338 -139T351 -118Q351 -106 337 -95T278 -78L255 -75L269 -8Q173 7 120 77T67 258Q67 320
|
||||
84 370T134 455T213 509T319 528Z" />
|
||||
<glyph unicode="è" glyph-name="egrave" d="M312 -12Q250 -12 202 7T122 61T72 145T55 257Q55 320 72 370T122 455T199 509T302 528Q358 528 403 510T479 457T528 375T545 269V227H183V214Q183 158 218 124T316 89Q364 89 398 108T456 160L529 87Q501 46
|
||||
448 17T312 -12ZM303 434Q249 434 216 400T183 310V303H417V312Q417 368 387 401T303 434ZM162 745L270 799L365 612L288 575L162 745Z" />
|
||||
<glyph unicode="é" glyph-name="eacute" d="M314 575L237 612L332 799L440 745L314 575ZM312 -12Q250 -12 202 7T122 61T72 145T55 257Q55 320 72 370T122 455T199 509T302 528Q358 528 403 510T479 457T528 375T545 269V227H183V214Q183 158 218 124T316
|
||||
89Q364 89 398 108T456 160L529 87Q501 46 448 17T312 -12ZM303 434Q249 434 216 400T183 310V303H417V312Q417 368 387 401T303 434Z" />
|
||||
<glyph unicode="ê" glyph-name="ecircumflex" d="M359 780L485 625L419 576L299 691L179 576L117 625L243 780H359ZM312 -12Q250 -12 202 7T122 61T72 145T55 257Q55 320 72 370T122 455T199 509T302 528Q358 528 403 510T479 457T528 375T545 269V227H183V214Q183
|
||||
158 218 124T316 89Q364 89 398 108T456 160L529 87Q501 46 448 17T312 -12ZM303 434Q249 434 216 400T183 310V303H417V312Q417 368 387 401T303 434Z" />
|
||||
<glyph unicode="ë" glyph-name="edieresis" d="M196 599Q155 599 139 616T123 657V679Q123 703 139 720T196 737Q237 737 253 720T269 679V657Q269 633 253 616T196 599ZM406 599Q365 599 349 616T333 657V679Q333 703 349 720T406 737Q447 737 463 720T479
|
||||
679V657Q479 633 463 616T406 599ZM312 -12Q250 -12 202 7T122 61T72 145T55 257Q55 320 72 370T122 455T199 509T302 528Q358 528 403 510T479 457T528 375T545 269V227H183V214Q183 158 218 124T316 89Q364 89 398 108T456 160L529 87Q501 46 448 17T312 -12ZM303
|
||||
434Q249 434 216 400T183 310V303H417V312Q417 368 387 401T303 434Z" />
|
||||
<glyph unicode="ì" glyph-name="igrave" d="M98 101H268V415H98V516H396V101H554V0H98V101ZM193 745L301 799L396 612L319 575L193 745Z" />
|
||||
<glyph unicode="í" glyph-name="iacute" d="M345 575L268 612L363 799L471 745L345 575ZM98 101H268V415H98V516H396V101H554V0H98V101Z" />
|
||||
<glyph unicode="î" glyph-name="icircumflex" d="M390 780L516 625L450 576L330 691L210 576L148 625L274 780H390ZM98 101H268V415H98V516H396V101H554V0H98V101Z" />
|
||||
<glyph unicode="ï" glyph-name="idieresis" d="M227 599Q186 599 170 616T154 657V679Q154 703 170 720T227 737Q268 737 284 720T300 679V657Q300 633 284 616T227 599ZM437 599Q396 599 380 616T364 657V679Q364 703 380 720T437 737Q478 737 494 720T510
|
||||
679V657Q510 633 494 616T437 599ZM98 101H268V415H98V516H396V101H554V0H98V101Z" />
|
||||
<glyph unicode="ð" glyph-name="eth" d="M476 698L397 648Q429 616 457 577T507 491T540 390T553 274Q553 200 535 146T484 57T405 5T303 -12Q243 -12 196 6T116 59T66 141T48 251Q48 308 62 354T104 433T169 483T253 501Q311 501 350 475T413 405L420 409Q403
|
||||
460 373 504T300 590L206 531L159 586L244 639Q209 668 170 692T87 740H284Q298 731 312 721T342 698L429 753L476 698ZM301 86Q356 86 388 119T421 215V274Q421 337 389 370T301 403Q246 403 214 370T181 274V215Q181 152 213 119T301 86Z" />
|
||||
<glyph unicode="ñ" glyph-name="ntilde" d="M75 0V516H203V425H208Q225 467 260 497T359 528Q435 528 482 478T530 333V0H402V315Q402 427 305 427Q285 427 267 422T235 407T212 381T203 345V0H75ZM377 598Q353 598 335 605T299 620Q277 630 259 636T224
|
||||
642Q207 642 194 636T165 617L119 676Q136 702 162 719T227 737Q251 737 269 730T305 715Q327 705 345 699T380 693Q397 693 410 699T439 718L485 659Q468 633 442 616T377 598Z" />
|
||||
<glyph unicode="ò" glyph-name="ograve" d="M300 -12Q241 -12 195 7T116 61T66 146T48 258Q48 320 65 370T115 455T194 509T300 528Q358 528 405 509T484 455T534 370T552 258Q552 196 535 146T485 61T405 7T300 -12ZM300 86Q355 86 387 119T419 218V298Q419
|
||||
363 387 396T300 430Q245 430 213 397T181 298V218Q181 153 213 120T300 86ZM161 745L269 799L364 612L287 575L161 745Z" />
|
||||
<glyph unicode="ó" glyph-name="oacute" d="M313 575L236 612L331 799L439 745L313 575ZM300 -12Q241 -12 195 7T116 61T66 146T48 258Q48 320 65 370T115 455T194 509T300 528Q358 528 405 509T484 455T534 370T552 258Q552 196 535 146T485 61T405 7T300
|
||||
-12ZM300 86Q355 86 387 119T419 218V298Q419 363 387 396T300 430Q245 430 213 397T181 298V218Q181 153 213 120T300 86Z" />
|
||||
<glyph unicode="ô" glyph-name="ocircumflex" d="M358 780L484 625L418 576L298 691L178 576L116 625L242 780H358ZM300 -12Q241 -12 195 7T116 61T66 146T48 258Q48 320 65 370T115 455T194 509T300 528Q358 528 405 509T484 455T534 370T552 258Q552 196
|
||||
535 146T485 61T405 7T300 -12ZM300 86Q355 86 387 119T419 218V298Q419 363 387 396T300 430Q245 430 213 397T181 298V218Q181 153 213 120T300 86Z" />
|
||||
<glyph unicode="õ" glyph-name="otilde" d="M300 -12Q241 -12 195 7T116 61T66 146T48 258Q48 320 65 370T115 455T194 509T300 528Q358 528 405 509T484 455T534 370T552 258Q552 196 535 146T485 61T405 7T300 -12ZM300 86Q355 86 387 119T419 218V298Q419
|
||||
363 387 396T300 430Q245 430 213 397T181 298V218Q181 153 213 120T300 86ZM375 598Q351 598 333 605T297 620Q275 630 257 636T222 642Q205 642 192 636T163 617L117 676Q134 702 160 719T225 737Q249 737 267 730T303 715Q325 705 343 699T378 693Q395 693 408
|
||||
699T437 718L483 659Q466 633 440 616T375 598Z" />
|
||||
<glyph unicode="ö" glyph-name="odieresis" d="M195 599Q154 599 138 616T122 657V679Q122 703 138 720T195 737Q236 737 252 720T268 679V657Q268 633 252 616T195 599ZM405 599Q364 599 348 616T332 657V679Q332 703 348 720T405 737Q446 737 462 720T478
|
||||
679V657Q478 633 462 616T405 599ZM300 -12Q241 -12 195 7T116 61T66 146T48 258Q48 320 65 370T115 455T194 509T300 528Q358 528 405 509T484 455T534 370T552 258Q552 196 535 146T485 61T405 7T300 -12ZM300 86Q355 86 387 119T419 218V298Q419 363 387 396T300
|
||||
430Q245 430 213 397T181 298V218Q181 153 213 120T300 86Z" />
|
||||
<glyph unicode="÷" glyph-name="divide" d="M62 254V357H538V254H62ZM300 36Q256 36 238 54T220 99V122Q220 148 238 166T300 185Q344 185 362 167T380 122V99Q380 72 362 54T300 36ZM300 426Q256 426 238 444T220 489V512Q220 538 238 556T300 575Q344 575
|
||||
362 557T380 512V489Q380 463 362 445T300 426Z" />
|
||||
<glyph unicode="ø" glyph-name="oslash" d="M30 -1L99 82Q48 149 48 258Q48 320 65 370T115 455T194 509T300 528Q340 528 374 519T436 493L499 569L570 517L501 434Q552 366 552 258Q552 196 535 146T485 61T405 7T300 -12Q260 -12 226 -3T164 23L101 -53L30
|
||||
-1ZM300 430Q245 430 213 397T181 298V224Q181 213 182 201T186 180L373 408Q359 419 341 424T300 430ZM300 86Q355 86 387 119T419 218V292Q419 303 418 315T414 336L227 108Q241 97 259 92T300 86Z" />
|
||||
<glyph unicode="ù" glyph-name="ugrave" d="M397 91H392Q375 49 340 19T241 -12Q165 -12 118 38T70 183V516H198V201Q198 89 295 89Q314 89 332 94T365 109T388 135T397 171V516H525V0H397V91ZM416 745L524 799L619 612L542 575L416 745Z" />
|
||||
<glyph unicode="ú" glyph-name="uacute" d="M568 575L491 612L586 799L694 745L568 575ZM397 91H392Q375 49 340 19T241 -12Q165 -12 118 38T70 183V516H198V201Q198 89 295 89Q314 89 332 94T365 109T388 135T397 171V516H525V0H397V91Z" />
|
||||
<glyph unicode="û" glyph-name="ucircumflex" d="M613 780L739 625L673 576L553 691L433 576L371 625L497 780H613ZM397 91H392Q375 49 340 19T241 -12Q165 -12 118 38T70 183V516H198V201Q198 89 295 89Q314 89 332 94T365 109T388 135T397 171V516H525V0H397V91Z" />
|
||||
<glyph unicode="ü" glyph-name="udieresis" d="M450 599Q409 599 393 616T377 657V679Q377 703 393 720T450 737Q491 737 507 720T523 679V657Q523 633 507 616T450 599ZM660 599Q619 599 603 616T587 657V679Q587 703 603 720T660 737Q701 737 717 720T733
|
||||
679V657Q733 633 717 616T660 599ZM397 91H392Q375 49 340 19T241 -12Q165 -12 118 38T70 183V516H198V201Q198 89 295 89Q314 89 332 94T365 109T388 135T397 171V516H525V0H397V91Z" />
|
||||
<glyph unicode="ý" glyph-name="yacute" d="M441 516H571L324 -92Q303 -145 268 -172T169 -200H75V-99H198L237 5L29 516H165L244 300L299 136H306L361 300L441 516ZM314 575L237 612L332 799L440 745L314 575Z" />
|
||||
<glyph unicode="þ" glyph-name="thorn" d="M69 740H197V425H204Q225 472 261 500T358 528Q402 528 438 512T501 462T541 377T555 258Q555 190 541 139T501 55T439 5T358 -12Q298 -12 262 16T204 91H197V-200H69V740ZM304 90Q360 90 390 124T421 217V299Q421
|
||||
358 391 392T304 426Q260 426 229 405T197 339V177Q197 133 228 112T304 90Z" />
|
||||
<glyph unicode="ÿ" glyph-name="ydieresis" d="M441 516H571L324 -92Q303 -145 268 -172T169 -200H75V-99H198L237 5L29 516H165L244 300L299 136H306L361 300L441 516ZM196 599Q155 599 139 616T123 657V679Q123 703 139 720T196 737Q237 737 253 720T269
|
||||
679V657Q269 633 253 616T196 599ZM406 599Q365 599 349 616T333 657V679Q333 703 349 720T406 737Q447 737 463 720T479 679V657Q479 633 463 616T406 599Z" />
|
||||
<glyph unicode="–" glyph-name="endash" d="M60 251V358H540V251H60Z" />
|
||||
<glyph unicode="—" glyph-name="emdash" d="M0 251V358H600V251H0Z" />
|
||||
<glyph unicode="‘" glyph-name="quoteleft" d="M303 740H399L344 428H172L303 740Z" />
|
||||
<glyph unicode="’" glyph-name="quoteright" d="M256 740H428L297 428H201L256 740Z" />
|
||||
<glyph unicode="‚" glyph-name="quotesinglbase" d="M236 163H408L277 -148H180L236 163Z" />
|
||||
<glyph unicode="“" glyph-name="quotedblleft" d="M436 740H532L477 428H305L436 740ZM431 740H527L472 428H300L431 740Z" />
|
||||
<glyph unicode="”" glyph-name="quotedblright" d="M389 740H561L430 428H334L389 740ZM384 740H556L425 428H329L384 740Z" />
|
||||
<glyph unicode="„" glyph-name="quotedblbase" d="M377 163H549L418 -148H321L377 163ZM372 163H544L413 -148H316L372 163Z" />
|
||||
<glyph unicode="•" glyph-name="bullet" d="M300 165Q260 165 233 174T188 200T164 239T156 288V320Q156 346 163 368T188 407T232 433T300 443Q340 443 367 434T412 408T436 369T444 320V288Q444 235 412 200T300 165Z" />
|
||||
<glyph unicode="‹" glyph-name="guilsinglleft" d="M374 43L172 213V329L374 499L413 415L278 271L413 127L374 43Z" />
|
||||
<glyph unicode="›" glyph-name="guilsinglright" d="M187 127L322 271L187 415L226 499L428 329V213L226 43L187 127Z" />
|
||||
</font>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 51 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user