diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index 565f9d0d..00000000 --- a/.appveyor.yml +++ /dev/null @@ -1,22 +0,0 @@ -platform: - - x64 - -clone_folder: c:\gopath\src\github.com\la5nta\pat - -environment: - global: - GOPATH: C:\gopath - GOVERSION: "1.16.5" - MSYS_PATH: C:\MinGW\msys\1.0 -install: - - set PATH=C:\go\bin;%MSYS_PATH%\bin;C:\MinGW\bin;%PATH% - - rmdir c:\go /s /q - - appveyor DownloadFile https://dl.google.com/go/go%GOVERSION%.windows-386.zip - - 7z x go%GOVERSION%.windows-386.zip -y -oC:\ > NUL -build_script: - - go version - - '%MSYS_PATH%\bin\bash -lc "cd /c/gopath/src/github.com/la5nta/pat && ./make.bash"' - -artifacts: - - path: pat.exe - name: Pat diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml new file mode 100644 index 00000000..82ca4937 --- /dev/null +++ b/.github/workflows/docker.yaml @@ -0,0 +1,42 @@ +name: docker-push + +on: + push: + branches: + - 'ci-test/*' + - 'release/*' + tags: + - 'v*' + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Generate Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: la5nta/pat + tags: | + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64,linux/386,linux/arm64/v8,linux/arm/v7,linux/arm/v6 + push: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/go.yaml b/.github/workflows/go.yaml new file mode 100644 index 00000000..8b57c280 --- /dev/null +++ b/.github/workflows/go.yaml @@ -0,0 +1,42 @@ +name: build +on: + push: + pull_request: + types: [ review_requested ] +jobs: + build: + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + go-version: [ '1.x' ] + include: + - os: ubuntu-latest + go-version: '1.19' + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v3 + - name: Setup Go ${{ matrix.go-version }} + uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.go-version }} + check-latest: true + cache: true + - if: ${{ matrix.os == 'ubuntu-latest' }} + name: Cache libax25 + id: cache-libax25 + uses: actions/cache@v3 + env: + cache-name: cache-libax25 + with: + path: .build + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ matrix.go-version }}-${{ hashFiles('make.bash') }} + restore-keys: ${{ runner.os }}-build-${{ env.cache-name }}-${{ matrix.go-version }}- + - if: ${{ matrix.os == 'ubuntu-latest' && steps.cache-libax25.outputs.cache-hit != 'true' }} + name: Setup libax25 + run: ./make.bash libax25 + - name: Display Go version + run: go version + - name: Vet + run: go vet ./... + - name: Build + run: ./make.bash diff --git a/.gitignore b/.gitignore index d6c02020..c0a8a7e7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .build/ pat +pat*.pkg +docker-data/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6c9dd42f..00000000 --- a/.travis.yml +++ /dev/null @@ -1,22 +0,0 @@ -language: go - -os: - - linux - - osx - -go: - - 1.16.x - - 1.x - -matrix: - exclude: - - os: osx - go: 1.16.x - -install: - - GO111MODULE=on go mod download - - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then ./make.bash libax25; fi - -script: - - go vet ./... - - ./make.bash diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8e471f3a..3f7da6c9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,7 +30,7 @@ To make the process as seamless as possible, we ask for the following: - Run `go fmt` - Consider squashing your commits into a single commit. `git rebase -i`. It's okay to force update your pull request. - **Write a good commit message.** This [blog article](http://chris.beams.io/posts/git-commit/) is a good resource for learning how to write good commit messages, the most important part being that each commit message should have a title/subject in imperative mood starting with a capital letter and no trailing period: *"Return error on wrong use of the Paginator"*, **NOT** *"returning some error."* Also, if your commit references one or more GitHub issues, always end your commit message body with *See #1234* or *Fixes #1234*. Replace *1234* with the GitHub issue ID. The last example will close the issue when the commit is merged into *master*. - - Make sure `go test ./...` passes, and `go build` completes. Our [Travis CI loop](https://travis-ci.org/la5nta/pat) (Linux and OS X) will catch most things that are missing. + - Make sure `go test ./...` passes, and `go build` completes. Our [Travis CI loop](https://app.travis-ci.com/github/la5nta/pat) (Linux and OS X) will catch most things that are missing. ## The release process diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..79a78ef1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM golang:alpine as builder +RUN apk add --no-cache git ca-certificates +WORKDIR /src +ADD go.mod go.sum ./ +RUN go mod download +ADD . . +RUN go build -o /src/pat + +FROM scratch +LABEL org.opencontainers.image.source=https://github.com/la5nta/pat +LABEL org.opencontainers.image.description="Pat - A portable Winlink client for amateur radio email" +LABEL org.opencontainers.image.licenses=MIT +COPY --from=builder /etc/ssl/certs /etc/ssl/certs +COPY --from=builder /src/pat /bin/pat +USER 65534:65534 +WORKDIR /app +ENV XDG_CONFIG_HOME=/app +ENV XDG_DATA_HOME=/app +ENV XDG_STATE_HOME=/app +ENV PAT_HTTPADDR=:8080 +EXPOSE 8080 +ENTRYPOINT ["/bin/pat"] +CMD ["http"] diff --git a/README.md b/README.md index b9b943e0..48dd566e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ -[![Build Status](https://travis-ci.com/la5nta/pat.svg?branch=master)](https://travis-ci.com/la5nta/pat) -[![Windows Build Status](https://ci.appveyor.com/api/projects/status/tstq4suxfdmudl5l/branch/master?svg=true)](https://ci.appveyor.com/project/martinhpedersen/pat) +[![Build status](https://github.com/la5nta/pat/actions/workflows/go.yaml/badge.svg)](https://github.com/la5nta/pat/actions) [![Go Report Card](https://goreportcard.com/badge/github.com/la5nta/pat)](https://goreportcard.com/report/github.com/la5nta/pat) [![Liberapay Patreons](http://img.shields.io/liberapay/patrons/la5nta.svg?logo=liberapay)](https://liberapay.com/la5nta) @@ -17,12 +16,12 @@ It is mainly developed for Linux, but is also known to run on OS X, Windows and * Message composer/reader (basic mailbox functionality). * Auto-shrink image attachments. * Post position reports with location from local GPS, browser location or manual entry. -* Rig control (using hamlib) for winmor PTT and QSY. +* Rig control (using hamlib). * CRON-like syntax for execution of scheduled commands (e.g. QSY or connect). * Built in http-server with web interface (mobile friendly). * Git style command line interface. * Listen for P2P connections using multiple modes concurrently. -* AX.25, telnet, WINMOR and ARDOP support. +* AX.25, telnet, PACTOR and ARDOP support. * Experimental gzip message compression (See "Gzip experiment" below). ##### Example @@ -65,6 +64,7 @@ Copyright (c) 2020 Martin Hebnes Pedersen LA5NTA * DL1THM - Torsten Harenberg * HB9GPA - Matthias Renner +* K0RET - Ryan Turner * K0SWE - Chris Keller * KD8DRX - Will Davidson * KE8HMG - Andrew Huebner @@ -72,9 +72,10 @@ Copyright (c) 2020 Martin Hebnes Pedersen LA5NTA * LA3QMA - Kai Günter Brandt * LA4TTA - Erlend Grimseid * LA5NTA - Martin Hebnes Pedersen +* N2YGK - Alan Crosswell +* VE7GNU - Doug Collinge * W6IPA - JC Martin * WY2K - Benjamin Seidenberg -* VE7GNU - Doug Collinge ## Thanks to diff --git a/cfg/ax25_engine.go b/cfg/ax25_engine.go new file mode 100644 index 00000000..387508dd --- /dev/null +++ b/cfg/ax25_engine.go @@ -0,0 +1,28 @@ +package cfg + +import ( + "encoding/json" + "fmt" +) + +const ( + AX25EngineAGWPE AX25Engine = "agwpe" + AX25EngineLinux = "linux" + AX25EngineSerialTNC = "serial-tnc" +) + +type AX25Engine string + +func (a *AX25Engine) UnmarshalJSON(p []byte) error { + var str string + if err := json.Unmarshal(p, &str); err != nil { + return err + } + switch v := AX25Engine(str); v { + case AX25EngineLinux, AX25EngineAGWPE, AX25EngineSerialTNC: + *a = v + return nil + default: + return fmt.Errorf("invalid AX.25 engine '%s'", v) + } +} diff --git a/cfg/ax25_engine_libax25.go b/cfg/ax25_engine_libax25.go new file mode 100644 index 00000000..87f4fa29 --- /dev/null +++ b/cfg/ax25_engine_libax25.go @@ -0,0 +1,6 @@ +//go:build libax25 +// +build libax25 + +package cfg + +func DefaultAX25Engine() AX25Engine { return AX25EngineLinux } diff --git a/cfg/ax25_engine_other.go b/cfg/ax25_engine_other.go new file mode 100644 index 00000000..99064004 --- /dev/null +++ b/cfg/ax25_engine_other.go @@ -0,0 +1,6 @@ +//go:build !libax25 +// +build !libax25 + +package cfg + +func DefaultAX25Engine() AX25Engine { return AX25EngineAGWPE } diff --git a/cfg/config.go b/cfg/config.go index d2cd93d6..2c0fc005 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -6,6 +6,9 @@ package cfg import ( "encoding/json" + "fmt" + "net" + "strconv" "strings" "github.com/la5nta/wl2k-go/transport/ardop" @@ -73,24 +76,27 @@ type Config struct { // Connect aliases // - // Example: {"LA1B-10": "ax25:///LD5GU/LA1B-10", "LA1B": "winmor://LA3F?freq=5350"} + // Example: {"LA1B-10": "ax25:///LD5GU/LA1B-10", "LA1B": "ardop://LA3F?freq=5350"} // Any occurrence of the substring "{mycall}" will be replaced with user's callsign. ConnectAliases map[string]string `json:"connect_aliases"` // Methods to listen for incoming P2P connections by default. // - // Example: ["ax25", "winmor", "telnet", "ardop"] + // Example: ["ax25", "telnet", "ardop"] Listen []string `json:"listen"` // Hamlib rigs available (with reference name) for ptt and frequency control. HamlibRigs map[string]HamlibConfig `json:"hamlib_rigs"` AX25 AX25Config `json:"ax25"` // See AX25Config. + AX25Linux AX25LinuxConfig `json:"ax25_linux"` // See AX25LinuxConfig. + AGWPE AGWPEConfig `json:"agwpe"` // See AGWPEConfig. SerialTNC SerialTNCConfig `json:"serial-tnc"` // See SerialTNCConfig. - Winmor WinmorConfig `json:"winmor"` // See WinmorConfig. Ardop ArdopConfig `json:"ardop"` // See ArdopConfig. Pactor PactorConfig `json:"pactor"` // See PactorConfig. Telnet TelnetConfig `json:"telnet"` // See TelnetConfig. + VaraHF VaraConfig `json:"varahf"` // See VaraConfig. + VaraFM VaraConfig `json:"varafm"` // See VaraConfig. // See GPSdConfig. GPSd GPSdConfig `json:"gpsd"` @@ -105,26 +111,20 @@ type Config struct { // # Connect to telnet once every hour // "@hourly": "connect telnet" // - // # Change winmor listen frequency based on hour of day - // "00 10 * * *": "freq winmor:7350.000", # 40m from 10:00 - // "00 18 * * *": "freq winmor:5347.000", # 60m from 18:00 - // "00 22 * * *": "freq winmor:3602.000" # 80m from 22:00 + // # Change ardop listen frequency based on hour of day + // "00 10 * * *": "freq ardop:7350.000", # 40m from 10:00 + // "00 18 * * *": "freq ardop:5347.000", # 60m from 18:00 + // "00 22 * * *": "freq ardop:3602.000" # 80m from 22:00 Schedule map[string]string `json:"schedule"` // By default, Pat posts your callsign and running version to the Winlink CMS Web Services // // Set to true if you don't want your information sent. VersionReportingDisabled bool `json:"version_reporting_disabled"` - - // Path to root of the Winlink Standard_Forms folder. - // Unzip after downloading from winlink.org - // - // Deprecated: in favor of the --forms flag - FormsPath string `json:"forms_path,omitempty"` } type HamlibConfig struct { - // The network type ("serial" or "tcp"). Use 'tcp' for rigctld. + // The network type ("serial" or "tcp"). Use 'tcp' for rigctld (default). // // (For serial support: build with "-tags libhamlib".) Network string `json:"network,omitempty"` @@ -139,45 +139,76 @@ type HamlibConfig struct { VFO string `json:"VFO"` } -type WinmorConfig struct { - // Network address of the Winmor TNC (e.g. localhost:8500). +type ArdopConfig struct { + // Network address of the Ardop TNC (e.g. localhost:8515). Addr string `json:"addr"` - // Bandwidth to use when getting an inbound connection (500/1600). - InboundBandwidth int `json:"inbound_bandwidth"` - - // TX audio drive level - // - // Set to 0 to use WINMOR defaults - DriveLevel int `json:"drive_level"` + // Default/listen ARQ bandwidth (200/500/1000/2000 MAX/FORCED). + ARQBandwidth ardop.Bandwidth `json:"arq_bandwidth"` // (optional) Reference name to the Hamlib rig to control frequency and ptt. Rig string `json:"rig"` // Set to true if hamlib should control PTT (SignaLink=false, most rigexpert=true). PTTControl bool `json:"ptt_ctrl"` + + // (optional) Send ID frame at a regular interval when the listener is active (unit is seconds) + BeaconInterval int `json:"beacon_interval"` + + // Send FSK CW ID after an ID frame. + CWID bool `json:"cwid_enabled"` } -type ArdopConfig struct { - // Network address of the Ardop TNC (e.g. localhost:8515). +type VaraConfig struct { + // Network host of the VARA modem (defaults to localhost:8300). Addr string `json:"addr"` - // ARQ bandwidth (200/500/1000/2000 MAX/FORCED). - ARQBandwidth ardop.Bandwidth `json:"arq_bandwidth"` + // Default/listen bandwidth (HF: 500/2300/2750 Hz). + Bandwidth int `json:"bandwidth"` // (optional) Reference name to the Hamlib rig to control frequency and ptt. Rig string `json:"rig"` // Set to true if hamlib should control PTT (SignaLink=false, most rigexpert=true). PTTControl bool `json:"ptt_ctrl"` +} - // (optional) Send ID frame at a regular interval when the listener is active (unit is seconds) - BeaconInterval int `json:"beacon_interval"` +// UnmarshalJSON implements VaraConfig JSON unmarshalling with support for legacy format. +func (v *VaraConfig) UnmarshalJSON(b []byte) error { + type newFormat VaraConfig + legacy := struct { + newFormat + Host string `json:"host"` + CmdPort int `json:"cmdPort"` + DataPort int `json:"dataPort"` + }{} + if err := json.Unmarshal(b, &legacy); err != nil { + return err + } + if legacy.newFormat.Addr == "" && legacy.Host != "" { + legacy.newFormat.Addr = fmt.Sprintf("%s:%d", legacy.Host, legacy.CmdPort) + } + *v = VaraConfig(legacy.newFormat) + if !v.IsZero() && v.CmdPort() <= 0 { + return fmt.Errorf("invalid addr format") + } + return nil +} - // Send FSK CW ID after an ID frame. - CWID bool `json:"cwid_enabled"` +func (v VaraConfig) IsZero() bool { return v == (VaraConfig{}) } + +func (v VaraConfig) Host() string { + host, _, _ := net.SplitHostPort(v.Addr) + return host } +func (v VaraConfig) CmdPort() int { + _, portStr, _ := net.SplitHostPort(v.Addr) + port, _ := strconv.Atoi(portStr) + return port +} +func (v VaraConfig) DataPort() int { return v.CmdPort() + 1 } + type PactorConfig struct { // Path/port to TNC device (e.g. /dev/ttyUSB0 or COM1). Path string `json:"path"` @@ -216,17 +247,41 @@ type SerialTNCConfig struct { // Type of TNC (currently only 'kenwood'). Type string `json:"type"` + + // (optional) Reference name to the Hamlib rig for frequency control. + Rig string `json:"rig"` +} + +type AGWPEConfig struct { + // The TCP address of the TNC. + Addr string `json:"addr"` + + // The AGWPE "radio port" (0-3). + RadioPort int `json:"radio_port"` } type AX25Config struct { - // axport to use (as defined in /etc/ax25/axports). - Port string `json:"port"` + // The AX.25 engine to be used. + // + // Valid options are: + // - linux + // - agwpe + // - serial-tnc + Engine AX25Engine `json:"engine"` + + // (optional) Reference name to the Hamlib rig for frequency control. + Rig string `json:"rig"` + + // DEPRECATED: See AX25Linux.Port. + AXPort string `json:"port,omitempty"` // Optional beacon when listening for incoming packet-p2p connections. Beacon BeaconConfig `json:"beacon"` +} - // (optional) Reference name to the Hamlib rig for frequency control. - Rig string `json:"rig"` +type AX25LinuxConfig struct { + // axport to use (as defined in /etc/ax25/axports). Only applicable to ax25 engine 'linux'. + Port string `json:"port"` } type BeaconConfig struct { @@ -241,16 +296,20 @@ type BeaconConfig struct { } type GPSdConfig struct { - // enable GPSd support in web interface - // WARNING: If you enable GPSd http endpoint (enable_http) you might - // expose your current position to anyone who has access to Pat!!! + // Enable GPSd proxy for HTTP (web GUI) + // + // Caution: Your GPS position will be accessible to any network device able to access Pat's HTTP interface. EnableHTTP bool `json:"enable_http"` - // Use server time instead of timestamp provided by GPSd (e.g for older GPS - // device with week roll-over issue) + // Allow Winlink forms to use GPSd for aquiring your position. + // + // Caution: Your current GPS position will be automatically injected, without your explicit consent, into forms requesting such information. + AllowForms bool `json:"allow_forms"` + + // Use server time instead of timestamp provided by GPSd (e.g for older GPS device with week roll-over issue). UseServerTime bool `json:"use_server_time"` - // Address and port of GPSd server (e.g. localhost:2947) + // Address and port of GPSd server (e.g. localhost:2947). Addr string `json:"addr"` } @@ -264,22 +323,25 @@ var DefaultConfig = Config{ Listen: []string{}, HTTPAddr: "localhost:8080", AX25: AX25Config{ - Port: "wl2k", + Engine: DefaultAX25Engine(), Beacon: BeaconConfig{ Every: 3600, Message: "Winlink P2P", Destination: "IDENT", }, }, + AX25Linux: AX25LinuxConfig{ + Port: "wl2k", + }, SerialTNC: SerialTNCConfig{ Path: "/dev/ttyUSB0", SerialBaud: 9600, HBaud: 1200, Type: "Kenwood", }, - Winmor: WinmorConfig{ - Addr: "localhost:8500", - InboundBandwidth: 1600, + AGWPE: AGWPEConfig{ + Addr: "localhost:8000", + RadioPort: 0, }, Ardop: ArdopConfig{ Addr: "localhost:8515", @@ -294,15 +356,20 @@ var DefaultConfig = Config{ ListenAddr: ":8774", Password: "", }, + VaraHF: VaraConfig{ + Addr: "localhost:8300", + Bandwidth: 2300, + }, + VaraFM: VaraConfig{ + Addr: "localhost:8300", + }, GPSd: GPSdConfig{ EnableHTTP: false, // Default to false to help protect privacy of unknowing users (see github.com//issues/146) + AllowForms: false, // Default to false to help protect location privacy of unknowing users UseServerTime: false, Addr: "localhost:2947", // Default listen address for GPSd }, GPSdAddrLegacy: "", Schedule: map[string]string{}, HamlibRigs: map[string]HamlibConfig{}, - - // Path to root of the unzipped Winlink Standard_Forms folder. - FormsPath: "", } diff --git a/cfg/example_config.json b/cfg/example_config.json deleted file mode 100644 index d9939f7f..00000000 --- a/cfg/example_config.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "mycall": "LA5NTA", - "secure_login_password": "my_not_so_secret_password", - "auxiliary_addresses": ["LE1OF"], - "locator": "JP20qe", - "connect_aliases": { - "LA1B@60m": "winmor:///LA1B?freq=5310.00", - "LA1B-10": "ax25:///LA1B-10" - }, - "hamlib_rigs": { - "ft897": { - "network": "tcp", - "address": "localhost:4532" - } - }, - "ax25": { - "port": "tmd710", - "beacon": { - "every": 3600, - "message": "Winlink P2P", - "destination": "IDENT" - } - }, - "serial-tnc": { - "path": "/dev/ttyUSB0", - "hbaud": 1200, - "serial_baud": 9600, - "type": "Kenwood" - }, - "winmor": { - "addr": "la5nta.local.mesh:8500", - "inbound_bandwidth": 500, - "rig": "ft897", - "ptt_ctrl": false - }, - "ardop": { - "addr": "localhost:8515", - "arq_bandwidth": {"Forced":false, "Max":500}, - "beacon_interval": 0, - "cwid_enabled": true, - "rig": "ft897", - "ptt_ctrl": false - }, - "telnet": { - "listen_addr": ":8774", - "password": "" - }, - "schedule": { - "*/30 * * * *": "connect offgrid" - }, - "gpsd": { - "enable_http": false, - "use_server_time": false, - "addr": "localhost:2947" - } -} diff --git a/cli_composer.go b/cli_composer.go index 5b5c1418..ef6e19b6 100644 --- a/cli_composer.go +++ b/cli_composer.go @@ -8,6 +8,7 @@ package main import ( "bufio" "bytes" + "context" "fmt" "io" "io/ioutil" @@ -102,11 +103,14 @@ func composeMessageHeader(replyMsg *fbb.Message) *fbb.Message { return msg } -func composeMessage(args []string) { +func composeMessage(ctx context.Context, args []string) { set := pflag.NewFlagSet("compose", pflag.ExitOnError) + // From default is --mycall but it can be overriden with -r + from := set.StringP("from", "r", fOptions.MyCall, "") subject := set.StringP("subject", "s", "", "") attachments := set.StringArrayP("attachment", "a", nil, "") ccs := set.StringArrayP("cc", "c", nil, "") + p2pOnly := set.BoolP("p2p-only", "", false, "") set.Parse(args) // Remaining args are recipients @@ -122,14 +126,13 @@ func composeMessage(args []string) { // Check if any args are set. If so, go non-interactive // Otherwise, interactive if (len(*subject) + len(*attachments) + len(*ccs) + len(recipients)) > 0 { - noninteractiveComposeMessage(*subject, *attachments, *ccs, recipients) + noninteractiveComposeMessage(*from, *subject, *attachments, *ccs, recipients, *p2pOnly) } else { interactiveComposeMessage(nil) } } -func noninteractiveComposeMessage(subject string, attachments []string, - ccs []string, recipients []string) { +func noninteractiveComposeMessage(from string, subject string, attachments []string, ccs []string, recipients []string, p2pOnly bool) { // We have to verify the args here. Follow the same pattern as main() // We'll allow a missing recipient if CC is present (or vice versa) if len(recipients)+len(ccs) <= 0 { @@ -143,7 +146,7 @@ func noninteractiveComposeMessage(subject string, attachments []string, } msg := fbb.NewMessage(fbb.Private, fOptions.MyCall) - msg.SetFrom(fOptions.MyCall) + msg.SetFrom(from) for _, to := range recipients { msg.AddTo(to) } @@ -171,11 +174,14 @@ func noninteractiveComposeMessage(subject string, attachments []string, } msg.SetBody(string(body)) + if p2pOnly { + msg.Header.Set("X-P2POnly", "true") + } postMessage(msg) } -// This is currently an alias for interactiveComposeMessage but keeping as a seperate +// This is currently an alias for interactiveComposeMessage but keeping as a separate // call path for the future func composeReplyMessage(replyMsg *fbb.Message) { interactiveComposeMessage(replyMsg) @@ -264,7 +270,7 @@ func readAttachment(path string) (*fbb.File, error) { name := filepath.Base(path) var resizeImage bool - if isImageMediaType(name, "") { + if isConvertableImageMediaType(name, "") { fmt.Print("This seems to be an image. Auto resize? [Y/n]: ") ans := readLine() resizeImage = ans == "" || strings.EqualFold("y", ans) @@ -293,7 +299,7 @@ func readLine() string { return strings.TrimSpace(str) } -func composeFormReport(args []string) { +func composeFormReport(ctx context.Context, args []string) { var tmplPathArg string set := pflag.NewFlagSet("form", pflag.ExitOnError) @@ -304,7 +310,7 @@ func composeFormReport(args []string) { formMsg, err := formsMgr.ComposeForm(tmplPathArg, msg.Subject()) if err != nil { - log.Printf("failed to compose message for template %s", tmplPathArg) + log.Printf("failed to compose message for template: %v", err) return } @@ -326,8 +332,10 @@ func composeFormReport(args []string) { msg.SetBody(formMsg.Body) - attachmentFile := fbb.NewFile(formMsg.AttachmentName, []byte(formMsg.AttachmentXML)) - msg.AddFile(attachmentFile) + if xml := formMsg.AttachmentXML; xml != "" { + attachmentFile := fbb.NewFile(formMsg.AttachmentName, []byte(xml)) + msg.AddFile(attachmentFile) + } postMessage(msg) } diff --git a/config.go b/config.go index 4196410a..5884837f 100644 --- a/config.go +++ b/config.go @@ -5,24 +5,37 @@ package main import ( - "bytes" "encoding/json" "fmt" "log" "os" "path" - "path/filepath" "strings" + "github.com/kelseyhightower/envconfig" "github.com/la5nta/pat/cfg" + "github.com/la5nta/pat/internal/buildinfo" "github.com/la5nta/pat/internal/debug" ) func LoadConfig(cfgPath string, fallback cfg.Config) (config cfg.Config, err error) { config, err = ReadConfig(cfgPath) - if os.IsNotExist(err) { - return fallback, WriteConfig(fallback, cfgPath) - } else if err != nil { + switch { + case os.IsNotExist(err): + config = fallback + if err := WriteConfig(config, cfgPath); err != nil { + return config, err + } + case err != nil: + return config, err + } + + // Environment variables overrides values from the config file + if err := envconfig.Process(buildinfo.AppName, &config); err != nil { + return config, err + } + // Environment variables for hamlib rigs (custom syntax not handled by envconfig) + if err := readRigsFromEnv(&config.HamlibRigs); err != nil { return config, err } @@ -34,64 +47,119 @@ func LoadConfig(cfgPath string, fallback cfg.Config) (config cfg.Config, err err config.ConnectAliases["telnet"] = cfg.DefaultConfig.ConnectAliases["telnet"] } + // TODO: Remove after some release cycles (2023-05-21) + // Rewrite deprecated serial-tnc:// aliases to ax25-serial-tnc:// + var deprecatedAliases []string + for k, v := range config.ConnectAliases { + if !strings.HasPrefix(v, MethodSerialTNCDeprecated+"://") { + continue + } + deprecatedAliases = append(deprecatedAliases, k) + config.ConnectAliases[k] = strings.Replace(v, MethodSerialTNCDeprecated, MethodAX25SerialTNC, 1) + } + if len(deprecatedAliases) > 0 { + log.Printf("Alias(es) %s uses deprecated transport scheme %s://. Please use %s:// instead.", strings.Join(deprecatedAliases, ", "), MethodSerialTNCDeprecated, MethodAX25SerialTNC) + } + // Ensure ServiceCodes has a default value if len(config.ServiceCodes) == 0 { config.ServiceCodes = cfg.DefaultConfig.ServiceCodes } + // Ensure we have a default AX.25 engine + if config.AX25.Engine == "" { + config.AX25.Engine = cfg.DefaultAX25Engine() + } + + // Ensure we have a default AGWPE config + if config.AGWPE == (cfg.AGWPEConfig{}) { + config.AGWPE = cfg.DefaultConfig.AGWPE + } + + // Ensure we have a default AX.25 Linux config + if config.AX25Linux == (cfg.AX25LinuxConfig{}) { + config.AX25Linux = cfg.DefaultConfig.AX25Linux + } + // TODO: Remove after some release cycles (2023-04-30) + if v := config.AX25.AXPort; v != "" && v != config.AX25Linux.Port { + log.Println("Using deprecated configuration option ax25.port. Please set ax25_linux.port instead.") + config.AX25Linux.Port = v + } + // Ensure Pactor has a default value if config.Pactor == (cfg.PactorConfig{}) { config.Pactor = cfg.DefaultConfig.Pactor } - // TODO: Remove after some release cycles (2019-09-29) - if config.GPSdAddrLegacy != "" { - config.GPSd.Addr = config.GPSdAddrLegacy + // Ensure VARA FM and VARA HF has default values + if config.VaraHF.IsZero() { + config.VaraHF = cfg.DefaultConfig.VaraHF + } + if config.VaraFM.IsZero() { + config.VaraFM = cfg.DefaultConfig.VaraFM } - // TODO: config FormsPath is deprecated in favor of --forms flag - homeDir, _ := os.UserHomeDir() - formsOldDefault := filepath.Join(homeDir, ".wl2k", "Standard_Forms") - // Ignore the old default config value - if config.FormsPath == formsOldDefault { - config.FormsPath = "" + // Ensure GPSd has a default value + if config.GPSd == (cfg.GPSdConfig{}) { + config.GPSd = cfg.DefaultConfig.GPSd } - if config.FormsPath != "" { - log.Println("Using deprecated configuration option 'forms_path'. Please use --forms flag instead.") - // clean up FormsPath (normalizes trailing slashes, and embedded '.' ) - config.FormsPath = filepath.Clean(config.FormsPath) - config.FormsPath = strings.ReplaceAll(config.FormsPath, "\\", "/") + // TODO: Remove after some release cycles (2019-09-29) + if v := config.GPSdAddrLegacy; v != "" && v != config.GPSd.Addr { + log.Println("Using deprecated configuration option gpsd_addr. Please set gpsd.addr instead.") + config.GPSd.Addr = v } + // Ensure SerialTNC has a default hbaud and serialbaud + if config.SerialTNC.HBaud == 0 { + config.SerialTNC.HBaud = cfg.DefaultConfig.SerialTNC.HBaud + } + if config.SerialTNC.SerialBaud == 0 { + config.SerialTNC.SerialBaud = cfg.DefaultConfig.SerialTNC.SerialBaud + } // Compatibility for the old baudrate field for serial-tnc - if v := config.SerialTNC.BaudrateLegacy; v != 0 && config.SerialTNC.HBaud == 0 { + if v := config.SerialTNC.BaudrateLegacy; v != 0 && v != config.SerialTNC.HBaud { + // Since we changed the default value from 9600 to 1200, we can't warn about this without causing confusion. debug.Printf("Legacy serial_tnc.baudrate config detected (%d). Translating to serial_tnc.hbaud.", v) config.SerialTNC.HBaud = v - config.SerialTNC.BaudrateLegacy = 0 } + return config, nil } -func replaceDeprecatedCMSHostname(path string, data []byte) ([]byte, error) { - const o = "@server.winlink.org:8772/wl2k" - const n = "@cms.winlink.org:8772/wl2k" - - if !bytes.Contains(data, []byte(o)) { - return data, nil - } - - data = bytes.ReplaceAll(data, []byte(o), []byte(n)) - - f, err := os.Open(path) - if err != nil { - return data, err - } - stat, err := f.Stat() - f.Close() - if err != nil { - return data, err +// readRigsFromEnv reads hamlib rigs config from environment. +// Syntax: PAT_HAMLIB_RIGS_{rig name}_{ATTRIBUTE} +// _{ATTRIBUTE} is optional (defaults to _ADDRESS). +// Examples: +// - PAT_HAMLIB_RIGS_rig1_NETWORK=tcp +// - PAT_HAMLIB_RIGS_rig1_ADDRESS=localhost:8080 +// - PAT_HAMLIB_RIGS_rig1_VFO=A +// - PAT_HAMLIB_RIGS_rig2=localhost:8080 +func readRigsFromEnv(rigs *map[string]cfg.HamlibConfig) error { + prefix := strings.ToUpper(buildinfo.AppName) + "_HAMLIB_RIGS_" + for _, env := range os.Environ() { + attribute, value, _ := strings.Cut(env, "=") + if !strings.HasPrefix(attribute, prefix) { + continue + } + attribute = strings.TrimPrefix(attribute, prefix) + name, attribute, _ := strings.Cut(attribute, "_") + if *rigs == nil { + *rigs = make(map[string]cfg.HamlibConfig) + } + rig := (*rigs)[name] + switch attribute { + case "ADDRESS", "": + rig.Address = value + case "NETWORK": + rig.Network = value + case "VFO": + rig.VFO = value + default: + return fmt.Errorf("invalid attribute '%s' for rig '%s'", attribute, name) + } + (*rigs)[name] = rig } - return data, os.WriteFile(path, data, stat.Mode()) + return nil } func ReadConfig(path string) (config cfg.Config, err error) { @@ -99,16 +167,6 @@ func ReadConfig(path string) (config cfg.Config, err error) { if err != nil { return } - - // TODO: Remove after some release cycles (2017-11-09) - data, err = replaceDeprecatedCMSHostname(path, data) - if err != nil { - fmt.Println("Failed to rewrite deprecated CMS hostname:", err) - fmt.Println("Please update your config's 'telnet' connect alias manually to:") - fmt.Println(cfg.DefaultConfig.ConnectAliases["telnet"]) - fmt.Println("") - } - err = json.Unmarshal(data, &config) return } diff --git a/config_test.go b/config_test.go new file mode 100644 index 00000000..5d7653cc --- /dev/null +++ b/config_test.go @@ -0,0 +1,62 @@ +package main + +import ( + "os" + "strings" + "testing" + + "github.com/la5nta/pat/cfg" +) + +func TestReadRigsFromEnv(t *testing.T) { + const prefix = "PAT_HAMLIB_RIGS" + unset := func() { + for _, env := range os.Environ() { + key, _, _ := strings.Cut(env, "=") + if strings.HasPrefix(key, prefix) { + os.Unsetenv(key) + } + } + } + t.Run("simple", func(t *testing.T) { + defer unset() + var rigs map[string]cfg.HamlibConfig + os.Setenv(prefix+"_rig", "localhost:4532") + if err := readRigsFromEnv(&rigs); err != nil { + t.Fatal(err) + } + if got := rigs["rig"]; (got != cfg.HamlibConfig{Address: "localhost:4532"}) { + t.Fatalf("Got unexpected config: %#v", got) + } + }) + t.Run("with VFO", func(t *testing.T) { + defer unset() + var rigs map[string]cfg.HamlibConfig + os.Setenv(prefix+"_rig", "localhost:4532") + os.Setenv(prefix+"_rig_VFO", "A") + if err := readRigsFromEnv(&rigs); err != nil { + t.Fatal(err) + } + if got := rigs["rig"]; (got != cfg.HamlibConfig{Address: "localhost:4532", VFO: "A"}) { + t.Fatalf("Got unexpected config: %#v", got) + } + }) + t.Run("full", func(t *testing.T) { + defer unset() + var rigs map[string]cfg.HamlibConfig + os.Setenv(prefix+"_rig_ADDRESS", "/dev/ttyS0") + os.Setenv(prefix+"_rig_NETWORK", "serial") + os.Setenv(prefix+"_rig_VFO", "B") + if err := readRigsFromEnv(&rigs); err != nil { + t.Fatal(err) + } + expect := cfg.HamlibConfig{ + Address: "/dev/ttyS0", + Network: "serial", + VFO: "B", + } + if got := rigs["rig"]; got != expect { + t.Fatalf("Got unexpected config: %#v", got) + } + }) +} diff --git a/connect.go b/connect.go index 6c1639ea..df8614eb 100644 --- a/connect.go +++ b/connect.go @@ -5,27 +5,39 @@ package main import ( + "context" + "errors" "fmt" "log" "strconv" "strings" "time" + "github.com/la5nta/pat/cfg" + "github.com/la5nta/pat/internal/debug" + "github.com/harenber/ptc-go/v2/pactor" "github.com/la5nta/wl2k-go/transport" "github.com/la5nta/wl2k-go/transport/ardop" - "github.com/la5nta/wl2k-go/transport/winmor" + "github.com/la5nta/wl2k-go/transport/ax25/agwpe" + "github.com/n8jja/Pat-Vara/vara" - // Register other dialers + // Register stateless dialers _ "github.com/la5nta/wl2k-go/transport/ax25" _ "github.com/la5nta/wl2k-go/transport/telnet" ) var ( dialing *transport.URL // The connect URL currently being dialed (if any) - wmTNC *winmor.TNC // Pointer to the WINMOR TNC used by Listen and Connect - adTNC *ardop.TNC // Pointer to the ARDOP TNC used by Listen and Connect - pModem *pactor.Modem + + adTNC *ardop.TNC // Pointer to the ARDOP TNC used by Listen and Connect + agwpeTNC *agwpe.TNCPort // Pointer to the AGWPE TNC combined TNC and Port + pModem *pactor.Modem + varaHFModem *vara.Modem + varaFMModem *vara.Modem + + // Context cancellation function for aborting while dialing. + dialCancelFunc func() = func() {} ) func hasSSID(str string) bool { return strings.Contains(str, "-") } @@ -46,21 +58,39 @@ func Connect(connectStr string) (success bool) { return Connect(aliased) } + // Hack around bug in frontend which may occur if the status updates too quickly. + if websocketHub != nil { + defer func() { time.Sleep(time.Second); websocketHub.UpdateStatus() }() + } + + debug.Printf("connectStr: %s", connectStr) url, err := transport.ParseURL(connectStr) if err != nil { log.Println(err) return false } + // TODO: Remove after some release cycles (2023-05-21) + // Rewrite legacy serial-tnc scheme. + if url.Scheme == MethodSerialTNCDeprecated { + log.Printf("Transport scheme %s:// is deprecated, use %s:// instead.", MethodSerialTNCDeprecated, MethodAX25SerialTNC) + url.Scheme = MethodAX25SerialTNC + } + + // Rewrite the generic ax25:// scheme to use a specified AX.25 engine. + if url.Scheme == MethodAX25 { + url.Scheme = defaultAX25Method() + } + // Init TNCs switch url.Scheme { - case MethodArdop: - if err := initArdopTNC(); err != nil { + case MethodAX25AGWPE: + if err := initAGWPE(); err != nil { log.Println(err) return } - case MethodWinmor: - if err := initWinmorTNC(); err != nil { + case MethodArdop: + if err := initArdopTNC(); err != nil { log.Println(err) return } @@ -73,6 +103,16 @@ func Connect(connectStr string) (success bool) { log.Println(err) return } + case MethodVaraHF: + if err := initVaraHFModem(); err != nil { + log.Println(err) + return + } + case MethodVaraFM: + if err := initVaraFMModem(); err != nil { + log.Println(err) + return + } } // Set default userinfo (mycall) @@ -83,9 +123,9 @@ func Connect(connectStr string) (success bool) { // Set default host interface address if url.Host == "" { switch url.Scheme { - case MethodAX25: - url.Host = config.AX25.Port - case MethodSerialTNC: + case MethodAX25Linux: + url.Host = config.AX25Linux.Port + case MethodAX25SerialTNC: url.Host = config.SerialTNC.Path if hbaud := config.SerialTNC.HBaud; hbaud > 0 { url.Params.Set("hbaud", fmt.Sprint(hbaud)) @@ -107,13 +147,11 @@ func Connect(connectStr string) (success bool) { return } - switch url.Scheme { - case MethodAX25, MethodSerialTNC: + if strings.HasPrefix(url.Scheme, MethodAX25) { log.Printf("Radio-Only is not available for %s", url.Scheme) return - default: - url.SetUser(url.User.Username() + "-T") } + url.SetUser(url.User.Username() + "-T") } // QSY @@ -136,29 +174,30 @@ func Connect(connectStr string) (success bool) { switch url.Scheme { case MethodArdop: waitBusy(adTNC) - case MethodWinmor: - waitBusy(wmTNC) } - // Catch interrupts (signals) while dialing, so users can abort ardop/winmor connects. - doneHandleInterrupt := handleInterrupt() + ctx, cancel := context.WithCancel(context.Background()) + dialCancelFunc = func() { dialing = nil; cancel() } + defer dialCancelFunc() // Signal web gui that we are dialing a connection dialing = url websocketHub.UpdateStatus() log.Printf("Connecting to %s (%s)...", url.Target, url.Scheme) - conn, err := transport.DialURL(url) + conn, err := transport.DialURLContext(ctx, url) // Signal web gui that we are no longer dialing dialing = nil websocketHub.UpdateStatus() - close(doneHandleInterrupt) - eventLog.LogConn("connect "+connectStr, currFreq, conn, err) - if err != nil { + switch { + case errors.Is(err, context.Canceled): + log.Printf("Connect cancelled") + return + case err != nil: log.Printf("Unable to establish connection to remote: %s", err) return } @@ -212,48 +251,6 @@ func waitBusy(b transport.BusyChannelChecker) { } } -func initWinmorTNC() error { - if wmTNC != nil && wmTNC.Ping() == nil { - return nil - } - - if wmTNC != nil { - wmTNC.Close() - } - - var err error - wmTNC, err = winmor.Open(config.Winmor.Addr, fOptions.MyCall, config.Locator) - if err != nil { - return fmt.Errorf("WINMOR TNC initialization failed: %w", err) - } - - if config.Winmor.DriveLevel != 0 { - if err := wmTNC.SetDriveLevel(config.Winmor.DriveLevel); err != nil { - log.Println("Failed to set WINMOR drive level:", err) - } - } - - if v, err := wmTNC.Version(); err != nil { - return fmt.Errorf("WINMOR TNC initialization failed: %s", err) - } else { - log.Printf("WINMOR TNC v%s initialized", v) - } - - transport.RegisterDialer(MethodWinmor, wmTNC) - - if !config.Winmor.PTTControl { - return nil - } - - rig, ok := rigs[config.Winmor.Rig] - if !ok { - return fmt.Errorf("unable to set PTT rig '%s': not defined or not loaded", config.Winmor.Rig) - } - wmTNC.SetPTT(rig) - - return nil -} - func initArdopTNC() error { if adTNC != nil && adTNC.Ping() == nil { return nil @@ -314,3 +311,103 @@ func initPactorModem(cmdlineinit string) error { return nil } + +func initVaraHFModem() error { + if varaHFModem != nil && varaHFModem.Ping() { + return nil + } + if varaHFModem != nil { + varaHFModem.Close() + } + m, err := initVaraModem(MethodVaraHF, config.VaraHF) + if err != nil { + return err + } + if bw := config.VaraHF.Bandwidth; bw != 0 { + if err := m.SetBandwidth(fmt.Sprint(bw)); err != nil { + m.Close() + return err + } + } + varaHFModem = m + return nil +} + +func initVaraFMModem() error { + if varaFMModem != nil && varaFMModem.Ping() { + return nil + } + if varaFMModem != nil { + varaFMModem.Close() + } + m, err := initVaraModem(MethodVaraFM, config.VaraFM) + if err != nil { + return err + } + varaFMModem = m + return nil +} + +func initVaraModem(scheme string, conf cfg.VaraConfig) (*vara.Modem, error) { + vConf := vara.ModemConfig{ + Host: conf.Host(), + CmdPort: conf.CmdPort(), + DataPort: conf.DataPort(), + } + m, err := vara.NewModem(scheme, fOptions.MyCall, vConf) + if err != nil { + return nil, fmt.Errorf("vara initialization failed: %w", err) + } + transport.RegisterDialer(scheme, m) + + if conf.PTTControl { + rig, ok := rigs[conf.Rig] + if !ok { + m.Close() + return nil, fmt.Errorf("unable to set PTT rig '%s': not defined or not loaded", conf.Rig) + } + m.SetPTT(rig) + } + v, _ := m.Version() + log.Printf("VARA modem (%s) initialized", v) + return m, nil +} + +func initAGWPE() error { + if agwpeTNC != nil && agwpeTNC.Ping() == nil { + return nil + } + + if agwpeTNC != nil { + agwpeTNC.Close() + } + + var err error + agwpeTNC, err = agwpe.OpenPortTCP(config.AGWPE.Addr, config.AGWPE.RadioPort, fOptions.MyCall) + if err != nil { + return fmt.Errorf("AGWPE TNC initialization failed: %w", err) + } + + if v, err := agwpeTNC.Version(); err != nil { + return fmt.Errorf("AGWPE TNC initialization failed: %w", err) + } else { + log.Printf("AGWPE TNC (%s) initialized", v) + } + + transport.RegisterContextDialer(MethodAX25AGWPE, agwpeTNC) + return nil +} + +// defaultAX25Method resolves the generic ax25:// scheme to a implementation specific scheme. +func defaultAX25Method() string { + switch config.AX25.Engine { + case cfg.AX25EngineAGWPE: + return MethodAX25AGWPE + case cfg.AX25EngineSerialTNC: + return MethodAX25SerialTNC + case cfg.AX25EngineLinux: + return MethodAX25Linux + default: + panic(fmt.Sprintf("invalid ax25 engine: %s", config.AX25.Engine)) + } +} diff --git a/convert_image.go b/convert_image.go index e1459efe..c67329c9 100644 --- a/convert_image.go +++ b/convert_image.go @@ -17,7 +17,7 @@ import ( "github.com/nfnt/resize" ) -func isImageMediaType(filename, contentType string) bool { +func isConvertableImageMediaType(filename, contentType string) bool { var mediaType string if contentType != "" { mediaType, _, _ = mime.ParseMediaType(contentType) @@ -26,7 +26,13 @@ func isImageMediaType(filename, contentType string) bool { mediaType = mime.TypeByExtension(path.Ext(filename)) } - return strings.HasPrefix(mediaType, "image/") + switch mediaType { + case "image/svg+xml": + // This is a text file + return false + default: + return strings.HasPrefix(mediaType, "image/") + } } func convertImage(orig []byte) ([]byte, error) { diff --git a/debian/changelog b/debian/changelog deleted file mode 100644 index f1d45d3c..00000000 --- a/debian/changelog +++ /dev/null @@ -1,270 +0,0 @@ -pat (0.12.1-1) stable; urgency=medium - - * Add support for configurable telnet dial timeout (for Iridium GO users) - * Add support for scriptable message composition - * Add CLI command `env` for retrieving related environment variables (for scripting) - * More reliable Forms updates by using a new API for retrieving latest version and archive URL - * Improve websocket handling - * Fix bug in Forms update procedure that would delete the OS temp directory in rare cases - * Fix bug with pactor serial communication on macOS (Darwin) - * Fix bug with Web GUI and Message IDs containing the hash (`#`) symbol - - -- Martin Hebnes Pedersen Sat, 11 Dec 2021 15:14:22 +0100 - -pat (0.12.0) stable; urgency=medium - - * Follow the XDG Base Directory Specification - * Add support for sending in precedence order - * Add new serial-tnc baudrate configuration options - * Fix bug in forms parsing leading to missing forms - * Fix permissions issue when updating forms - * Fix FBB protocol handshake issue - * Improve fsnotify handling for mailbox events - * More descriptive error on premature disconnect - * Add basic debug logging capabilities - * Various dependency updates and refactoring - - -- Martin Hebnes Pedersen Sun, 31 Oct 2021 17:28:02 +0100 - -pat (0.11.0) stable; urgency=medium - - * Add support for Winlink HTML Forms - * Add support for individual passords for auxiliary addresses - * Add ability to abort ongoing dialing/connection in Web GUI - * Add systemd unit file for rigctld - * Improve version reporing to Winlink API - * Improve websocket handling - * Improve visibility of QSY errors in Web GUI - * Improve 'reply' and 'forward' functionality in Web GUI - * Fix issue with azimuth calculation when distance is zero - * Fix incorrect transport URI scheme for packet nodes - * Fix build on FreeBSD and macOS. - * Avoid truncating rmslist cache on refresh failure - * Avoid recompressing images where the resulting file size increases - * Require Go 1.16 or later - - -- Martin Hebnes Pedersen Wed, 30 Jun 2021 21:13:40 +0100 - -pat (0.10.0) stable; urgency=medium - - * Add support for P4 Dragon modems - * Add RMS list viewer in Web GUI's connect modal - * Add support for additional connect parameters for pactor - * New max length of message attachment filenames (255 characters) - - -- Martin Hebnes Pedersen Thu, 08 Sep 2020 19:39:40 +0100 - -pat (0.9.0) stable; urgency=medium - - * Less aggressive websocket timeout - * Add column sorting in Web GUI - * Require Go 1.10 or later - * Fix GPSd config bug introduced in v0.8.0 - * Fix (mainly macOS) bug related to many open file descriptors - - -- Martin Hebnes Pedersen Wed, 19 Feb 2020 20:13:18 +0100 - -pat (0.8.0) stable; urgency=medium - - * GPSd support in Web GUI - * User configurable Service Code - * High Accuracy HTML5 Geolocation - * Minor PACTOR enhancements and bug fixes - * Fixed ARDOP listener issue - - -- Martin Hebnes Pedersen Thu, 03 Oct 2019 21:48:51 +0200 - -pat (0.7.0) stable; urgency=medium - - * Support PACTOR PTC-II and PTC-III (https://github.com/la5nta/pat/issues/40) - * Fix QSY frequency rounding error (https://github.com/la5nta/pat/issues/147) - * Fix panic on ARDOP TNC connection teardown (https://github.com/la5nta/pat/issues/137) - * Fix ARDOP compatibility issue (https://github.com/la5nta/pat/issues/139) - - -- Martin Hebnes Pedersen Wed, 18 Sep 2019 21:56:17 +0200 - -pat (0.6.1) stable; urgency=medium - - * Add deb package `dist` as conflicting package (https://github.com/la5nta/pat/issues/131) - * Include systemd unit file for ARDOPc (https://github.com/la5nta/pat/issues/130) - * Set correct URL parameter for serial-tnc.Baudrate (https://github.com/la5nta/pat/issues/129) - * Fix Go 1.10 compatibility issue (https://github.com/la5nta/pat/issues/121) - - -- Martin Hebnes Pedersen Sun, 21 Apr 2018 11:23:40 +0200 - -pat (0.6.0) stable; urgency=high - - * Support Winlink's new mixed-case password scheme (https://github.com/la5nta/pat/issues/113) - * Support for distance and azumuth in rmslist (https://github.com/la5nta/pat/pull/112) - * Improved ARDOP ID-frame parser - - -- Martin Hebnes Pedersen Mon, 22 Jan 2018 21:41:13 +0100 - -pat (0.5.1) stable; urgency=medium - - * Support ARDOP >= v1.0 (https://github.com/la5nta/pat/issues/108) - * Add rmslist support for ARDOP nodes - * Switch to the new Winlink rest API (https://github.com/la5nta/pat/issues/110) - * Fix bug which caused WINMOR connection failure when dialing the (non-idle) TNC - - -- Martin Hebnes Pedersen Tue, 12 Dec 2017 19:03:04 +0100 - -pat (0.5.0) stable; urgency=high - - * Fix XSS vulnerability when serving attachments over HTTP (https://github.com/la5nta/pat/issues/105) - * Gracefully recover/initialize failed external devices (https://github.com/la5nta/pat/issues/88) - * Switch to the new Winlink CMS and API hostname (https://github.com/la5nta/pat/issues/104) - * Add config option for WINMOR's Drive Level parameter (https://github.com/la5nta/pat/issues/99) - * Add password prompt in web GUI (https://github.com/la5nta/pat/issues/90) - * Include man pages in deb and pkg packages (https://github.com/la5nta/pat/pull/91) - * Various minor web GUI improvements (https://github.com/la5nta/pat/issues/97) - - -- Martin Hebnes Pedersen Sat, 18 Nov 2017 11:40:28 +0100 - -pat (0.4.0) stable; urgency=medium - - * Desktop notifications for web GUI users (https://github.com/la5nta/pat/issues/85) - * New status indicator in web GUI for display of various alerts and info (https://github.com/la5nta/pat/issues/86) - * Add Cc field to the web GUI composer (https://github.com/la5nta/pat/issues/83) - * Tokenize address input in the web GUI composer (https://github.com/la5nta/pat/issues/84) - * Check for empty To/Cc on compose (https://github.com/la5nta/pat/issues/89) - - -- Martin Hebnes Pedersen Tue, 17 Sep 2017 11:14:59 +0200 - -pat (0.3.0) stable; urgency=high (Fixes compatibility with an upcoming Winlink CMS release) - - * Fix critical compatibility issues with WL2K-4.0 aka "AWS-CMS" (https://github.com/la5nta/pat/issues/81) - * Fix close of AX.25 listener on Linux (https://github.com/la5nta/pat/issues/68) - * Add "Delete" and "Move to archive" actions in web GUI (https://github.com/la5nta/pat/issues/63) - - -- Martin Hebnes Pedersen Tue, 18 Jul 2017 21:13:08 +0200 - -pat (0.2.4) stable; urgency=medium - - * Add progress bar for message transfer in web GUI (https://github.com/la5nta/pat/pull/78) - * Properly parse offset in B2 compressed message header for BPQ compatibility (https://github.com/la5nta/pat/issues/74) - * Fix libax25 segfault on invalid axport (https://github.com/la5nta/pat/issues/73) - * Silence FREQUENCY parse errors for ardop (https://github.com/la5nta/pat/issues/75) - - -- Martin Hebnes Pedersen Tue, 28 Feb 2017 19:07:00 +0100 - -pat (0.2.3) stable; urgency=medium - - * Support ARDOP >= v0.9 (https://github.com/la5nta/pat/issues/69) - * Improve list parsing in various UI fields - * Handle non-ascii attachment names - - -- Martin Hebnes Pedersen Fri, 27 Jan 2016 18:17:30 +0100 - -pat (0.2.2) stable; urgency=medium - - * Ensure default config is written before opening the configuration editor (https://github.com/la5nta/pat/issues/70) - * Add some missing config defaults - - -- Martin Hebnes Pedersen Thu, 01 Dec 2016 18:14:09 +0100 - -pat (0.2.1) stable; urgency=medium - - * Support ARDOP >= v0.6 (https://github.com/la5nta/pat/issues/60) - * Fix bug that caused 'configure' to fail if config format was invalid (https://github.com/la5nta/pat/issues/62) - * Add position format examples for --latlon (https://github.com/la5nta/pat/issues/65) - * Statically link libax25 (linux) to avoid crash on incompatible shared library (https://github.com/la5nta/pat/issues/59) - - -- Martin Hebnes Pedersen Wed, 12 Oct 2016 20:24:18 +0200 - -pat (0.2.0) stable; urgency=medium - - * Support Radio only - Winlink Hybrid Network (https://github.com/la5nta/pat/issues/44) - * Swich to Go port of lzhuf (https://github.com/la5nta/pat/issues/50) - * Linux ax25 scripts: Add method for custom TNC initialization (https://github.com/la5nta/pat/issues/53) - * Fix ardop PTT rigcontrol (https://github.com/la5nta/pat/issues/58) - * Minor bug fixes and improvements in the web GUI - - -- Martin Hebnes Pedersen Fri, 05 Aug 2016 15:16:51 +0200 - -pat (0.1.5) stable; urgency=medium - - * Fix bug that caused command-line interface composer's prompt scan to see whitespace as end of line (https://github.com/la5nta/pat/issues/45) - * Fix Mac OS default install path (https://github.com/la5nta/pat/issues/47) - - -- Martin Hebnes Pedersen Mon, 27 Jun 2016 22:43:36 +0200 - -pat (0.1.4) stable; urgency=medium - - * Fix case where secure_login_password was ignored if mycall was not all upper case (https://github.com/la5nta/pat/issues/42) - * Support image resize in cli composer (https://github.com/la5nta/pat/issues/38) - * Remove imagemagick dependency for image resize (https://github.com/la5nta/pat/issues/13) - * Minor improvement of cli mailbox navigation (https://github.com/la5nta/pat/issues/39) - - -- Martin Hebnes Pedersen Thu, 09 Jun 2016 21:02:42 +0200 - -pat (0.1.3) stable; urgency=medium - - * Add filename extension for mailbox messages (https://github.com/la5nta/pat/issues/34) - * Fix broken ax25:// digipeater syntax (https://github.com/la5nta/pat/issues/33) - * Enable gzip experiment by default (https://github.com/la5nta/pat/issues/29) - - -- Martin Hebnes Pedersen Sat, 07 May 2016 22:18:12 +0200 - -pat (0.1.2) stable; urgency=medium - - * Fix callsign casing bug (https://github.com/la5nta/pat/issues/19) - * Fix web composer Re: prefix issues in replies (https://github.com/la5nta/pat/issues/30) - * Support running http server while in interactive mode (https://github.com/la5nta/pat/issues/26) - * Send smallest messages first (suggested in the Winlink FAQ) (https://github.com/la5nta/pat/issues/25) - * Fix handling of proposal code H (https://github.com/la5nta/pat/issues/25) - * Fix handling of blocks with all messages deferred/rejected (https://github.com/la5nta/pat/issues/25) - * Fix unstable serialization of messages that could result in corrupt partial message transfer (https://github.com/la5nta/pat/issues/25) - * Support both utf8 and iso-8859-1 encoded subject header (https://github.com/la5nta/pat/issues/23) - * Re-implement ctrl+c for aborting connect/session (https://github.com/la5nta/pat/issues/22) - * Fix GUI post button issues on some browsers (https://github.com/la5nta/pat/issues/21) - * Fix WINMOR unexpected EOF issue on session termination (https://github.com/la5nta/pat/issues/20) - * Fix improper handling of callsign casing (https://github.com/la5nta/pat/issues/19) - - -- Martin Hebnes Pedersen Sat, 02 Apr 2016 10:41:16 +0200 - -pat (0.1.1) stable; urgency=medium - - * Fix various file locking errors on Windows (https://github.com/la5nta/pat/issues/9). - * Automatic version reporting to Winlink CMS Web Services. - - -- Martin Hebnes Pedersen Fri, 11 Mar 2016 21:06:16 +0100 - -pat (0.1.0) stable; urgency=medium - - * Initial release under new name. - * Fix leak that caused increasing CPU load. - * Add band filtering for rmslist command. - * Fix winmor robust issues. - - -- Martin Hebnes Pedersen Sun, 06 Mar 2016 14:09:11 +0100 - -wl2k-go (0.0.4) stable; urgency=medium - - * Fixed parse error of Date field from RMS Relay'ed messages (https://github.com/la5nta/wl2k-go/issues/29). - * Fixed parse of ax25 URLs with digipeaters (https://github.com/la5nta/wl2k-go/issues/28). - * Fixed panic on misconfigured (empty) axport (https://github.com/la5nta/wl2k-go/issues/27). - * Prompt user for login password if mycall is overridden by --mycall even though a password is defined in config. - * Run winmor in robust mode during handshake and proposal chatter. - * GPSd support (for position reporting using a local serial/usb GPS). - - -- Martin Hebnes Pedersen Sun, 14 Feb 2016 18:19:02 +0100 - -wl2k-go (0.0.3) stable; urgency=medium - - * Fixed web ui assets bug (https://github.com/la5nta/wl2k-go/issues/26). - * Fixed systemd user install script. - - -- Martin Hebnes Pedersen Thu, 14 Jan 2016 19:26:49 +0100 - -wl2k-go (0.0.2) stable; urgency=medium - - * Fixed ARDOPc issues. - - -- Martin Hebnes Pedersen Sun, 10 Jan 2016 15:56:00 +0100 - -wl2k-go (0.0.1) stable; urgency=medium - - * Initial release. - - -- Martin Hebnes Pedersen Sun, 04 Nov 2016 16:24:24 +0100 diff --git a/debian/control b/debian/control deleted file mode 100644 index 7e8d21fe..00000000 --- a/debian/control +++ /dev/null @@ -1,15 +0,0 @@ -Source: pat -Section: ham -Priority: extra -Maintainer: Martin Hebnes Pedersen -Homepage: http://getpat.io -Build-Depends: debhelper (>= 7.0.50~), golang (>= 2:1.16), libax25, libax25-dev -Standards-Version: 3.9.1 - -Package: pat -Architecture: arm64 amd64 i386 armhf -Conflicts: wl2k-go, dist -Replaces: wl2k-go -Recommends: libhamlib-utils (>= 1.2), ax25-tools, gpsd (>= 2.90) -Suggests: tmd710-tncsetup -Description: A portable Winlink client for amateur radio email. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..cff5d5ed --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +services: + pat: + image: la5nta/pat + build: . + volumes: + - ./docker-data:/app/pat + ports: + - 8080:8080 diff --git a/env.go b/env.go index d8821a18..44fff081 100644 --- a/env.go +++ b/env.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "io" "os" @@ -9,7 +10,7 @@ import ( "github.com/la5nta/pat/internal/buildinfo" ) -func envHandle(_ []string) { +func envHandle(_ context.Context, _ []string) { writeEnvAll(os.Stdout) } @@ -25,9 +26,15 @@ func writeEnvAll(w io.Writer) { writeEnv(w, "PAT_EVENTLOG_PATH", fOptions.EventLogPath) writeEnv(w, "PAT_FORMS_PATH", fOptions.FormsPath) writeEnv(w, "PAT_DEBUG", os.Getenv("PAT_DEBUG")) + writeEnv(w, "PAT_WEB_DEV_ADDR", os.Getenv("PAT_WEB_DEV_ADDR")) + writeEnv(w, "ARDOP_DEBUG", os.Getenv("ARDOP_DEBUG")) - writeEnv(w, "WINMOR_DEBUG", os.Getenv("WINMOR_DEBUG")) writeEnv(w, "PACTOR_DEBUG", os.Getenv("PACTOR_DEBUG")) + writeEnv(w, "AGWPE_DEBUG", os.Getenv("AGWPE_DEBUG")) + writeEnv(w, "VARA_DEBUG", os.Getenv("VARA_DEBUG")) + + writeEnv(w, "GZIP_EXPERIMENT", os.Getenv("GZIP_EXPERIMENT")) + writeEnv(w, "ARDOP_FSKONLY_EXPERIMENT", os.Getenv("ARDOP_FSKONLY_EXPERIMENT")) } func writeEnv(w io.Writer, k, v string) { diff --git a/exchange.go b/exchange.go index c001357e..d999806b 100644 --- a/exchange.go +++ b/exchange.go @@ -9,7 +9,6 @@ import ( "log" "net" "os" - "os/signal" "strings" "time" @@ -93,7 +92,7 @@ func sessionExchange(conn net.Conn, targetCall string, master bool) error { return config.SecureLoginPassword, nil } for _, aux := range config.AuxAddrs { - if addr.Addr != aux.Address { + if !addr.EqualString(aux.Address) { continue } switch { @@ -122,10 +121,6 @@ func sessionExchange(conn net.Conn, targetCall string, master bool) error { log.Printf("Connected to %s (%s)", conn.RemoteAddr(), conn.RemoteAddr().Network()) - // Close connection on os.Interrupt - stop := handleInterrupt() - defer close(stop) - start := time.Now() stats, err := session.Exchange(conn) @@ -163,71 +158,74 @@ func sessionExchange(conn net.Conn, targetCall string, master bool) error { return err } -func handleInterrupt() (stop chan struct{}) { - stop = make(chan struct{}) - - go func() { - sig := make(chan os.Signal, 1) - signal.Notify(sig, os.Interrupt) - defer func() { signal.Stop(sig); close(sig) }() - - dirtyDisconnectNext := false // So we can do a dirty disconnect on the second interrupt - for { - select { - case <-stop: - return - case <-sig: - abortActiveConnection(dirtyDisconnectNext) - dirtyDisconnectNext = !dirtyDisconnectNext - } - } - }() - - return stop -} - func abortActiveConnection(dirty bool) (ok bool) { switch { + case dirty: + // This mean we've already tried to abort, but the connection is still active. + // Fallback to the below cases to try to identify the busy modem and abort hard. + case dialing != nil: + // If we're currently dialing a transport, attempt to abort by cancelling the associated context. + log.Printf("Got abort signal while dialing %s, cancelling...", dialing.Scheme) + go dialCancelFunc() + return true case exchangeConn != nil: + // If we have an active connection, close it gracefully. log.Println("Got abort signal, disconnecting...") - exchangeConn.Close() + go exchangeConn.Close() return true - case pModem != nil: - log.Println("Disconnecting pactor...") - err := pModem.Close() - if err != nil { - log.Println(err) + } + + // Any connection and/or dial operation has been cancelled at this point. + // User is attempting to abort something, so try to identify any non-idling transports and abort. + // It might be a "dirty disconnect" of an already cancelled connection or dial operation which is in the + // process of gracefully terminating. It might also be an attempt to close an inbound P2P connection. + switch { + case adTNC != nil && !adTNC.Idle(): + if dirty { + log.Println("Dirty disconnecting ardop...") + adTNC.Abort() + return true } - return err == nil - case wmTNC != nil && !wmTNC.Idle(): + log.Println("Disconnecting ardop...") + go func() { + if err := adTNC.Disconnect(); err != nil { + log.Println(err) + } + }() + return true + case varaFMModem != nil && !varaFMModem.Idle(): if dirty { - log.Println("Dirty disconnecting winmor...") - wmTNC.DirtyDisconnect() + log.Println("Dirty disconnecting varafm...") + varaFMModem.Abort() return true } - log.Println("Disconnecting winmor...") + log.Println("Disconnecting varafm...") go func() { - if err := wmTNC.Disconnect(); err != nil { + if err := varaFMModem.Close(); err != nil { log.Println(err) } }() return true - case adTNC != nil && !adTNC.Idle(): + case varaHFModem != nil && !varaHFModem.Idle(): if dirty { - log.Println("Dirty disconnecting ardop...") - adTNC.Abort() + log.Println("Dirty disconnecting varahf...") + varaHFModem.Abort() return true } - log.Println("Disconnecting ardop...") + log.Println("Disconnecting varahf...") go func() { - if err := adTNC.Disconnect(); err != nil { + if err := varaHFModem.Close(); err != nil { log.Println(err) } }() return true - case dialing != nil: - log.Printf("Transport %s's dialer can not be aborted at this stage", dialing.Scheme) - return false + case pModem != nil: + log.Println("Disconnecting pactor...") + err := pModem.Close() + if err != nil { + log.Println(err) + } + return err == nil default: return false } diff --git a/flags.go b/flags.go index d320d404..dd7319cc 100644 --- a/flags.go +++ b/flags.go @@ -5,6 +5,7 @@ package main import ( + "context" "fmt" "os" "strings" @@ -18,7 +19,7 @@ type Command struct { Str string Aliases []string Desc string - HandleFunc func(args []string) + HandleFunc func(ctx context.Context, args []string) Usage string Options map[string]string Example string diff --git a/freq.go b/freq.go index 4677adf7..14b24594 100644 --- a/freq.go +++ b/freq.go @@ -68,16 +68,16 @@ func (f Frequency) KHz() float64 { return float64(f) / 1e3 } func (f Frequency) Dial(mode string) Frequency { mode = strings.ToLower(mode) - // Try to detect FM modes - // (ARDOP on FM is reported as `ARDOP 2000 FM`) - if strings.HasSuffix(mode, "fm") { + // Try to detect FM modes, e.g. `ARDOP 2000 FM` and `VARA FM WIDE` + if strings.Contains(mode, "fm") { return f } offsets := map[string]Frequency{ - MethodWinmor: 1500, MethodPactor: 1500, MethodArdop: 1500, + // varahf doesn't appear in RMS list from WDT + "vara": 1500, } var shift Frequency @@ -93,15 +93,17 @@ func (f Frequency) Dial(mode string) Frequency { func VFOForTransport(transport string) (vfo hamlib.VFO, rigName string, ok bool, err error) { var rig string - switch transport { - case MethodWinmor: - rig = config.Winmor.Rig - case MethodArdop: + switch { + case transport == MethodArdop: rig = config.Ardop.Rig - case MethodAX25: + case transport == MethodAX25, strings.HasPrefix(transport, MethodAX25+"+"): rig = config.AX25.Rig - case MethodPactor: + case transport == MethodPactor: rig = config.Pactor.Rig + case transport == MethodVaraHF: + rig = config.VaraHF.Rig + case transport == MethodVaraFM: + rig = config.VaraFM.Rig default: return vfo, "", false, fmt.Errorf("not supported with transport '%s'", transport) } diff --git a/go.mod b/go.mod index f85f13f9..b5238483 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/la5nta/pat -go 1.16 +go 1.19 require ( github.com/adrg/xdg v0.3.3 @@ -10,19 +10,31 @@ require ( github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75 github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.4.2 - github.com/harenber/ptc-go/v2 v2.2.2 + github.com/harenber/ptc-go/v2 v2.2.3 github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c - github.com/la5nta/wl2k-go v0.9.0 - github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/kelseyhightower/envconfig v1.4.0 + github.com/la5nta/wl2k-go v0.11.8 github.com/microcosm-cc/bluemonday v1.0.16 + github.com/n8jja/Pat-Vara v1.1.4 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 github.com/pd0mz/go-maidenhead v1.0.0 github.com/peterh/liner v1.2.1 github.com/spf13/pflag v1.0.5 - go.uber.org/atomic v1.9.0 // indirect - go.uber.org/multierr v1.7.0 // indirect +) + +require ( + dario.cat/mergo v1.0.0 // indirect + github.com/albenik/go-serial/v2 v2.6.0 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/creack/goselect v0.1.2 // indirect + github.com/gorilla/css v1.0.0 // indirect + github.com/howeyc/crc16 v0.0.0-20171223171357-2b2a61e366a6 // indirect + github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/paulrosania/go-charset v0.0.0-20190326053356-55c9d7a5834c // indirect + github.com/rivo/uniseg v0.2.0 // indirect + go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.0.0-20210813211128-0a44fdfbc16e // indirect golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d // indirect - golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e // indirect + golang.org/x/sys v0.13.0 // indirect golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect ) diff --git a/go.sum b/go.sum index de7c55cf..849ab08d 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,11 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/adrg/xdg v0.3.3 h1:s/tV7MdqQnzB1nKY8aqHvAMD+uCiuEDzVB5HLRY849U= github.com/adrg/xdg v0.3.3/go.mod h1:61xAR2VZcggl2St4O9ohF5qCKe08+JDmE4VNzPFQvOQ= -github.com/albenik/go-serial/v2 v2.3.0/go.mod h1:JUrQKdczCMB0FlXt2rlJJ8zbfFzmjTIAkLPyyVfr5ho= -github.com/albenik/go-serial/v2 v2.4.0 h1:2yIM9C0l0YznmMbHF8Yw+j0XY1ZDy5YciqhGdOik6r8= github.com/albenik/go-serial/v2 v2.4.0/go.mod h1:JUrQKdczCMB0FlXt2rlJJ8zbfFzmjTIAkLPyyVfr5ho= +github.com/albenik/go-serial/v2 v2.5.0/go.mod h1:ySdCqoERscw1xluK1n62R8Faoyu+jXKwVHPa1lSSAew= +github.com/albenik/go-serial/v2 v2.6.0 h1:UX30WZPL0qouDrKu4xwVFgvQA3YDTNhk3+aVC6X0jYg= +github.com/albenik/go-serial/v2 v2.6.0/go.mod h1:sqQA6eeZHKUB6rAgrBsP/8d3Go5Md5cjCof1WcyaK0o= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/bndr/gotabulate v1.1.3-0.20170315142410-bc555436bfd5 h1:D48YSLPNJ8WpdwDqYF8bMMKUB2bgdWEiFx1MGwPIdbs= @@ -25,24 +28,29 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/harenber/ptc-go/v2 v2.2.2 h1:b6+QwjtdV2bDvhFgM5tksNMx5zN/Ml9inldF7msS0Zw= -github.com/harenber/ptc-go/v2 v2.2.2/go.mod h1:SDIy4XqnUq6YYPcLLjDTfbLPf1/Z82ho1VPkPGVXSTo= +github.com/harenber/ptc-go/v2 v2.2.3 h1:saGN1zhWozAF2kNseDI9YCHwuCl1Seb3++gkCwVfcj8= +github.com/harenber/ptc-go/v2 v2.2.3/go.mod h1:SDIy4XqnUq6YYPcLLjDTfbLPf1/Z82ho1VPkPGVXSTo= github.com/howeyc/crc16 v0.0.0-20171223171357-2b2a61e366a6 h1:IIVxLyDUYErC950b8kecjoqDet8P5S4lcVRUOM6rdkU= github.com/howeyc/crc16 v0.0.0-20171223171357-2b2a61e366a6/go.mod h1:JslaLRrzGsOKJgFEPBP65Whn+rdwDQSk0I0MCRFe2Zw= github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c h1:aY2hhxLhjEAbfXOx2nRJxCXezC6CO2V/yN+OCr1srtk= github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/la5nta/wl2k-go v0.7.3/go.mod h1:rTQaxPiAFD3pWGWN8Lh+BskN3Fpii84GoVwpTHNiCjE= -github.com/la5nta/wl2k-go v0.9.0 h1:kdeUrGXLocisDpDvIkmtnXbHGopYiM6Pei/6rnYAGSs= -github.com/la5nta/wl2k-go v0.9.0/go.mod h1:m/O3yYdsWhPdM1K/nAXqqioWRQGJwgKO7D8pEk6AQeY= +github.com/la5nta/wl2k-go v0.11.5/go.mod h1:0c+/9KyDj7Ra7C/O4rVUYx1CzvdtS65di/93wlI22fo= +github.com/la5nta/wl2k-go v0.11.8 h1:fTrOYm7oJu/b+3RmQMGX9TfpADnrFFkLzkDpfRTaEIs= +github.com/la5nta/wl2k-go v0.11.8/go.mod h1:rUK5mVAldeSuru47APLp9wJMJ5BiaZZ3YxZafSNs6CI= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/microcosm-cc/bluemonday v1.0.16 h1:kHmAq2t7WPWLjiGvzKa5o3HzSfahUKiOq7fAPUiMNIc= github.com/microcosm-cc/bluemonday v1.0.16/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM= +github.com/n8jja/Pat-Vara v1.1.4 h1:yXqQjQQmpcXc9dA5XjRVvC1eYaFoErxvFeIHzLlPA90= +github.com/n8jja/Pat-Vara v1.1.4/go.mod h1:9ovT5w1MeVtQ336AqhoPmgiQ4eGDgNiygBxFvAiSJbc= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/paulrosania/go-charset v0.0.0-20151028000031-621bb39fcc83/go.mod h1:YnNlZP7l4MhyGQ4CBRwv6ohZTPrUJJZtEv4ZgADkbs4= @@ -60,18 +68,17 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/tarm/goserial v0.0.0-20151007205400-b3440c3c6355/go.mod h1:jcMo2Odv5FpDA6rp8bnczbUolcICW6t54K3s9gOlgII= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec= go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20210813211128-0a44fdfbc16e h1:VvfwVmMH40bpMeizC9/K7ipM5Qjucuu16RWfneFPyhQ= golang.org/x/crypto v0.0.0-20210813211128-0a44fdfbc16e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d h1:LO7XpTYMwTqxjLcGWPijK3vRXg1aWdlNOVOHRq45d7c= golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -79,17 +86,17 @@ golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210223212115-eede4237b368/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e h1:WUoyKPm6nCo1BnNUvPGnFG3T5DUVem42yDJZZ4CNxMA= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/http.go b/http.go index f60bfdb7..11dea589 100644 --- a/http.go +++ b/http.go @@ -7,6 +7,7 @@ package main import ( "bufio" "bytes" + "context" "embed" "encoding/json" "errors" @@ -18,6 +19,7 @@ import ( "mime/multipart" "net" "net/http" + "net/http/httputil" "net/url" "os" "path" @@ -27,6 +29,8 @@ import ( "strings" "time" + "github.com/la5nta/wl2k-go/transport/ardop" + "github.com/la5nta/pat/internal/buildinfo" "github.com/la5nta/pat/internal/gpsd" @@ -36,9 +40,10 @@ import ( "github.com/la5nta/wl2k-go/fbb" "github.com/la5nta/wl2k-go/mailbox" "github.com/microcosm-cc/bluemonday" + "github.com/n8jja/Pat-Vara/vara" ) -//go:embed web/res/** +//go:embed web/dist/** var embeddedFS embed.FS var staticContent fs.FS @@ -84,7 +89,9 @@ func init() { } } -func ListenAndServe(addr string) error { +func devServerAddr() string { return strings.TrimSuffix(os.Getenv("PAT_WEB_DEV_ADDR"), "/") } + +func ListenAndServe(ctx context.Context, addr string) error { log.Printf("Starting HTTP service (http://%s)...", addr) if host, _, _ := net.SplitHostPort(addr); host == "" && config.GPSd.EnableHTTP { @@ -94,6 +101,7 @@ func ListenAndServe(addr string) error { } r := mux.NewRouter() + r.HandleFunc("/api/bandwidths", bandwidthsHandler).Methods("GET") r.HandleFunc("/api/connect_aliases", connectAliasesHandler).Methods("GET") r.HandleFunc("/api/connect", ConnectHandler) r.HandleFunc("/api/formcatalog", formsMgr.GetFormsCatalogHandler).Methods("GET") @@ -113,16 +121,46 @@ func ListenAndServe(addr string) error { r.HandleFunc("/api/current_gps_position", positionHandler).Methods("GET") r.HandleFunc("/api/qsy", qsyHandler).Methods("POST") r.HandleFunc("/api/rmslist", rmslistHandler).Methods("GET") + + r.PathPrefix("/dist/").Handler(distHandler()) r.HandleFunc("/ws", wsHandler) - r.HandleFunc("/ui", uiHandler).Methods("GET") + r.HandleFunc("/ui", uiHandler()).Methods("GET") r.HandleFunc("/", rootHandler).Methods("GET") - http.Handle("/", r) - http.Handle("/res/", http.FileServer(http.FS(staticContent))) - websocketHub = NewWSHub() - return http.ListenAndServe(addr, nil) + srv := http.Server{ + Addr: addr, + Handler: r, + } + errs := make(chan error, 1) + go func() { + errs <- srv.ListenAndServe() + }() + + select { + case <-ctx.Done(): + log.Println("Shutting down HTTP server...") + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + srv.Shutdown(ctx) + return nil + case err := <-errs: + return err + } +} + +func distHandler() http.Handler { + switch target := devServerAddr(); { + case target != "": + targetURL, err := url.Parse(target) + if err != nil { + log.Fatalf("invalid proxy target URL: %v", err) + } + return httputil.NewSingleHostReverseProxy(targetURL) + default: + return http.FileServer(http.FS(staticContent)) + } } func rootHandler(w http.ResponseWriter, r *http.Request) { @@ -246,8 +284,10 @@ func postOutboundMessageHandler(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie("forminstance") if err == nil { formData := formsMgr.GetPostedFormData(cookie.Value) - name := formsMgr.GetXMLAttachmentNameForForm(formData.TargetForm, formData.IsReply) - msg.AddFile(fbb.NewFile(name, []byte(formData.MsgXML))) + if xml := formData.MsgXML; xml != "" { + name := formsMgr.GetXMLAttachmentNameForForm(formData.TargetForm, formData.IsReply) + msg.AddFile(fbb.NewFile(name, []byte(formData.MsgXML))) + } } // Other fields @@ -304,7 +344,7 @@ func attachFile(f *multipart.FileHeader, msg *fbb.Message) error { // For some unknown reason, we receive this empty unnamed file when no // attachment is provided. Prior to Go 1.10, this was filtered by // multipart.Reader. - if isEmptyFormFile(f) { + if f.Size == 0 && f.Filename == "" { return nil } @@ -323,7 +363,7 @@ func attachFile(f *multipart.FileHeader, msg *fbb.Message) error { return HTTPError{err, http.StatusInternalServerError} } - if isImageMediaType(f.Filename, f.Header.Get("Content-Type")) { + if isConvertableImageMediaType(f.Filename, f.Header.Get("Content-Type")) { log.Printf("Auto converting '%s' [%s]...", f.Filename, f.Header.Get("Content-Type")) if converted, err := convertImage(p); err != nil { @@ -355,23 +395,36 @@ func wsHandler(w http.ResponseWriter, r *http.Request) { websocketHub.Handle(conn) } -func uiHandler(w http.ResponseWriter, _ *http.Request) { - data, err := fs.ReadFile(staticContent, path.Join("res", "tmpl", "index.html")) - if err != nil { - log.Fatal(err) - } - - t := template.New("index.html") // create a new template - t, err = t.Parse(string(data)) - if err != nil { - log.Fatal(err) +func uiHandler() http.HandlerFunc { + const indexPath = "dist/index.html" + templateFunc := func() ([]byte, error) { return fs.ReadFile(staticContent, indexPath) } + if target := devServerAddr(); target != "" { + templateFunc = func() ([]byte, error) { + resp, err := http.Get(target + "/" + indexPath) + if err != nil { + return nil, fmt.Errorf("dev server not reachable: %w", err) + } + defer resp.Body.Close() + return io.ReadAll(resp.Body) + } } - tmplData := struct{ AppName, Version, Mycall string }{buildinfo.AppName, buildinfo.VersionString(), fOptions.MyCall} - - err = t.Execute(w, tmplData) - if err != nil { - log.Fatal(err) + return func(w http.ResponseWriter, _ *http.Request) { + data, err := templateFunc() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + t, err := template.New("index.html").Parse(string(data)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + tmplData := struct{ AppName, Version, Mycall string }{buildinfo.AppName, buildinfo.VersionString(), fOptions.MyCall} + if err := t.Execute(w, tmplData); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } } } @@ -400,13 +453,38 @@ func statusHandler(w http.ResponseWriter, _ *http.Request) { _ = json.NewEncoder(w).Encode(getStatus()) } +func bandwidthsHandler(w http.ResponseWriter, req *http.Request) { + type BandwidthResponse struct { + Mode string `json:"mode"` + Bandwidths []string `json:"bandwidths"` + Default string `json:"default,omitempty"` + } + mode := strings.ToLower(req.FormValue("mode")) + resp := BandwidthResponse{Mode: mode, Bandwidths: []string{}} + switch { + case mode == MethodArdop: + for _, bw := range ardop.Bandwidths() { + resp.Bandwidths = append(resp.Bandwidths, bw.String()) + } + if bw := config.Ardop.ARQBandwidth; !bw.IsZero() { + resp.Default = bw.String() + } + case mode == MethodVaraHF: + resp.Bandwidths = vara.Bandwidths() + if bw := config.VaraHF.Bandwidth; bw != 0 { + resp.Default = fmt.Sprintf("%d", bw) + } + } + _ = json.NewEncoder(w).Encode(resp) +} + func rmslistHandler(w http.ResponseWriter, req *http.Request) { forceDownload, _ := strconv.ParseBool(req.FormValue("force-download")) band := req.FormValue("band") mode := strings.ToLower(req.FormValue("mode")) prefix := strings.ToUpper(req.FormValue("prefix")) - list, err := ReadRMSList(forceDownload, func(r RMS) bool { + list, err := ReadRMSList(req.Context(), forceDownload, func(r RMS) bool { switch { case r.URL == nil: return false diff --git a/http_multipart.go b/http_multipart.go deleted file mode 100644 index d3c9cf78..00000000 --- a/http_multipart.go +++ /dev/null @@ -1,9 +0,0 @@ -// +build go1.10 - -package main - -import "mime/multipart" - -func isEmptyFormFile(f *multipart.FileHeader) bool { - return f.Size == 0 && f.Filename == "" -} diff --git a/interactive.go b/interactive.go index 5b198fc7..46ec4694 100644 --- a/interactive.go +++ b/interactive.go @@ -6,6 +6,7 @@ package main import ( "bytes" + "context" "fmt" "log" "os" @@ -17,24 +18,32 @@ import ( "github.com/peterh/liner" ) -func Interactive() { +func Interactive(ctx context.Context) { line := liner.NewLiner() defer line.Close() - for { - str, _ := line.Prompt(getPrompt()) - if str == "" { - continue - } - line.AppendHistory(str) - - if str[0] == '#' { - continue - } - - if quit := execCmd(str); quit { - break + done := make(chan struct{}) + go func() { + defer close(done) + for { + str, _ := line.Prompt(getPrompt()) + if str == "" { + continue + } + line.AppendHistory(str) + + if str[0] == '#' { + continue + } + + if quit := execCmd(str); quit { + break + } } + }() + select { + case <-ctx.Done(): + case <-done: } } @@ -60,7 +69,6 @@ func execCmd(line string) (quit bool) { PrintQTC() case "debug": os.Setenv("ardop_debug", "1") - os.Setenv("winmor_debug", "1") fmt.Println("Number of goroutines:", runtime.NumGoroutine()) case "q", "quit": return true @@ -76,11 +84,12 @@ func printInteractiveUsage() { fmt.Println("Uri examples: 'LA3F@5350', 'LA1B-10 v LA5NTA-1', 'LA5NTA:secret@192.168.1.1:54321'") methods := []string{ - MethodWinmor, MethodArdop, - MethodAX25, + MethodAX25, MethodAX25AGWPE, MethodAX25Linux, MethodAX25SerialTNC, + MethodPactor, MethodTelnet, - MethodSerialTNC, + MethodVaraHF, + MethodVaraFM, } fmt.Println("Methods:", strings.Join(methods, ", ")) @@ -116,17 +125,6 @@ func PrintHeard() { fmt.Printf(" %-10s (%s)\n", call, t.Format(time.RFC1123)) } - fmt.Println("winmor:") - if wmTNC == nil { - fmt.Println(" (not initialized)") - } else if heard := wmTNC.Heard(); len(heard) == 0 { - fmt.Println(" (none)") - } else { - for call, t := range heard { - pf(call, t) - } - } - fmt.Println("ardop:") if adTNC == nil { fmt.Println(" (not initialized)") @@ -138,8 +136,8 @@ func PrintHeard() { } } - fmt.Println("ax25:") - if heard, err := ax25.Heard(config.AX25.Port); err != nil { + fmt.Println("ax25+linux:") + if heard, err := ax25.Heard(config.AX25Linux.Port); err != nil { fmt.Printf(" (%s)\n", err) } else if len(heard) == 0 { fmt.Println(" (none)") diff --git a/internal/buildinfo/VERSION.go b/internal/buildinfo/VERSION.go index bee40cfe..440d12ff 100644 --- a/internal/buildinfo/VERSION.go +++ b/internal/buildinfo/VERSION.go @@ -6,10 +6,14 @@ package buildinfo const ( // AppName is the friendly name of the app. + // + // Forks should consider using a different name. AppName = "Pat" + // Version is the app's SemVer. - Version = "0.12.1" + // + // Forks should NOT bump this unless they use a unique AppName. The Winlink + // system uses this to derive the "these users should upgrade" wall of shame + // from CMS connects. + Version = "0.15.1" ) - -// GitRev is the git commit hash that the binary was built at. -var GitRev = "unknown origin" // Set by make.bash diff --git a/internal/buildinfo/gitrev.go b/internal/buildinfo/gitrev.go new file mode 100644 index 00000000..76435f90 --- /dev/null +++ b/internal/buildinfo/gitrev.go @@ -0,0 +1,18 @@ +//go:build go1.18 +// +build go1.18 + +package buildinfo + +import "runtime/debug" + +// GitRev is the git commit hash that the binary was built at. +var GitRev = func() string { + if info, ok := debug.ReadBuildInfo(); ok { + for _, setting := range info.Settings { + if setting.Key == "vcs.revision" && len(setting.Value) > 7 { + return setting.Value[:7] + } + } + } + return "unknown origin" +}() diff --git a/internal/buildinfo/gitrev_legacy.go b/internal/buildinfo/gitrev_legacy.go new file mode 100644 index 00000000..15bff940 --- /dev/null +++ b/internal/buildinfo/gitrev_legacy.go @@ -0,0 +1,7 @@ +//go:build !go1.18 +// +build !go1.18 + +package buildinfo + +// GitRev is the git commit hash that the binary was built at. +var GitRev = "unknown origin" // Set by make.bash diff --git a/internal/cmsapi/api.go b/internal/cmsapi/api.go index d43ad790..81078826 100644 --- a/internal/cmsapi/api.go +++ b/internal/cmsapi/api.go @@ -6,6 +6,7 @@ package cmsapi import ( "bytes" + "context" "encoding/json" "fmt" "io" @@ -13,6 +14,7 @@ import ( "net/http" "net/url" "os" + "strings" "time" ) @@ -107,10 +109,10 @@ type RFC1123Time struct{ time.Time } // GetGatewayStatus fetches the gateway status list returned by GatewayStatusUrl // -// mode can be any of [packet, pactor, winmor, robustpacket, allhf or anyall]. Empty is AnyAll. +// mode can be any of [packet, pactor, robustpacket, allhf or anyall]. Empty is AnyAll. // historyHours is the number of hours of history to include (maximum: 48). If < 1, then API default is used. // serviceCodes defaults to "PUBLIC". -func GetGatewayStatus(mode string, historyHours int, serviceCodes ...string) (io.ReadCloser, error) { +func GetGatewayStatus(ctx context.Context, mode string, historyHours int, serviceCodes ...string) (io.ReadCloser, error) { switch { case mode == "": mode = "AnyAll" @@ -129,7 +131,12 @@ func GetGatewayStatus(mode string, historyHours int, serviceCodes ...string) (io params.Add("ServiceCodes", str) } - resp, err := http.PostForm(RootURL+PathGatewayStatus, params) + req, err := http.NewRequestWithContext(ctx, "POST", RootURL+PathGatewayStatus, strings.NewReader(params.Encode())) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + resp, err := http.DefaultClient.Do(req) switch { case err != nil: return nil, err @@ -140,7 +147,7 @@ func GetGatewayStatus(mode string, historyHours int, serviceCodes ...string) (io return resp.Body, err } -func GetGatewayStatusCached(cacheFile string, forceDownload bool, serviceCodes ...string) (io.ReadCloser, error) { +func GetGatewayStatusCached(ctx context.Context, cacheFile string, forceDownload bool, serviceCodes ...string) (io.ReadCloser, error) { if !forceDownload { file, err := os.Open(cacheFile) if err == nil { @@ -149,7 +156,7 @@ func GetGatewayStatusCached(cacheFile string, forceDownload bool, serviceCodes . } log.Println("Downloading latest gateway status information...") - fresh, err := GetGatewayStatus("", 48, serviceCodes...) + fresh, err := GetGatewayStatus(ctx, "", 48, serviceCodes...) if err != nil { return nil, err } diff --git a/internal/forms/date.go b/internal/forms/date.go new file mode 100644 index 00000000..af0af8e4 --- /dev/null +++ b/internal/forms/date.go @@ -0,0 +1,14 @@ +package forms + +import ( + "strings" + "time" +) + +func formatDateTime(t time.Time) string { return t.Format("2006-01-02 15:04:05") } +func formatDateTimeUTC(t time.Time) string { return t.UTC().Format("2006-01-02 15:04:05Z07:00") } +func formatDate(t time.Time) string { return t.Format("2006-01-02") } +func formatTime(t time.Time) string { return t.Format("15:04:05") } +func formatDateUTC(t time.Time) string { return t.UTC().Format("2006-01-02Z07:00") } +func formatTimeUTC(t time.Time) string { return t.UTC().Format("15:04:05Z07:00") } +func formatUDTG(t time.Time) string { return strings.ToUpper(t.UTC().Format("021504Z07:00 Jan 2006")) } diff --git a/internal/forms/date_test.go b/internal/forms/date_test.go new file mode 100644 index 00000000..c5434da9 --- /dev/null +++ b/internal/forms/date_test.go @@ -0,0 +1,29 @@ +package forms + +import ( + "testing" + "time" +) + +func TestDateFormat(t *testing.T) { + now := time.Date(2023, 12, 31, 23, 59, 59, 0, time.FixedZone("UTC-4", -4*60*60)) + + tests := []struct { + fn func(t time.Time) string + expect string + }{ + {formatDateTime, "2023-12-31 23:59:59"}, + {formatDateTimeUTC, "2024-01-01 03:59:59Z"}, + {formatDate, "2023-12-31"}, + {formatTime, "23:59:59"}, + {formatDateUTC, "2024-01-01Z"}, + {formatTimeUTC, "03:59:59Z"}, + {formatUDTG, "010359Z JAN 2024"}, + } + + for i, tt := range tests { + if got := tt.fn(now); got != tt.expect { + t.Errorf("%d: got %q expected %q", i, got, tt.expect) + } + } +} diff --git a/internal/forms/forms.go b/internal/forms/forms.go index 6a5a2255..3eaadfd1 100644 --- a/internal/forms/forms.go +++ b/internal/forms/forms.go @@ -10,6 +10,7 @@ import ( "archive/zip" "bufio" "bytes" + "context" "encoding/json" "encoding/xml" "errors" @@ -17,6 +18,7 @@ import ( "io" "io/ioutil" "log" + "math" "net/http" "os" "path" @@ -30,10 +32,15 @@ import ( "unicode/utf8" "github.com/dimchansky/utfbom" + "github.com/la5nta/pat/cfg" + "github.com/la5nta/pat/internal/debug" + "github.com/la5nta/pat/internal/gpsd" + "github.com/pd0mz/go-maidenhead" ) const ( fieldValueFalseInXML = "False" + htmlFileExt = ".html" txtFileExt = ".txt" formsVersionInfoURL = "https://api.getpat.io/v1/forms/standard-templates/latest" ) @@ -59,6 +66,7 @@ type Config struct { AppVersion string LineReader func() string UserAgent string + GPSd cfg.GPSdConfig } // Form holds information about a Winlink form template @@ -86,6 +94,8 @@ type FormFolder struct { type FormData struct { TargetForm Form `json:"target_form"` Fields map[string]string `json:"fields"` + MsgTo string `json:"msg_to"` + MsgCc string `json:"msg_cc"` MsgSubject string `json:"msg_subject"` MsgBody string `json:"msg_body"` MsgXML string `json:"msg_xml"` @@ -95,6 +105,8 @@ type FormData struct { // MessageForm represents a concrete form-based message type MessageForm struct { + To string + Cc string Subject string Body string AttachmentXML string @@ -187,6 +199,8 @@ func (m *Manager) PostFormDataHandler(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusBadRequest) log.Printf("%s %s: %s", r.Method, r.URL.Path, err) } + formData.MsgTo = formMsg.To + formData.MsgCc = formMsg.Cc formData.MsgSubject = formMsg.Subject formData.MsgBody = formMsg.Body formData.MsgXML = formMsg.AttachmentXML @@ -214,9 +228,8 @@ func (m *Manager) GetFormDataHandler(w http.ResponseWriter, r *http.Request) { // GetPostedFormData is similar to GetFormDataHandler, but used when posting the form-based message to the outbox func (m *Manager) GetPostedFormData(key string) FormData { m.postedFormData.RLock() - retVal := m.postedFormData.internalFormDataMap[key] - m.postedFormData.RUnlock() - return retVal + defer m.postedFormData.RUnlock() + return m.postedFormData.internalFormDataMap[key] } // GetFormTemplateHandler handles the request for viewing a form filled-in with instance values @@ -251,8 +264,8 @@ func (m *Manager) GetFormTemplateHandler(w http.ResponseWriter, r *http.Request) } // UpdateFormTemplatesHandler handles API calls to update form templates. -func (m *Manager) UpdateFormTemplatesHandler(w http.ResponseWriter, _ *http.Request) { - response, err := m.UpdateFormTemplates() +func (m *Manager) UpdateFormTemplatesHandler(w http.ResponseWriter, r *http.Request) { + response, err := m.UpdateFormTemplates(r.Context()) if err != nil { http.Error(w, fmt.Sprint(err), http.StatusInternalServerError) return @@ -262,14 +275,12 @@ func (m *Manager) UpdateFormTemplatesHandler(w http.ResponseWriter, _ *http.Requ } // UpdateFormTemplates handles searching for and installing the latest version of the form templates. -func (m *Manager) UpdateFormTemplates() (UpdateResponse, error) { - if _, err := os.Stat(m.config.FormsPath); err != nil { - if err := os.MkdirAll(m.config.FormsPath, 0o755); err != nil { - return UpdateResponse{}, fmt.Errorf("can't write to forms dir [%w]", err) - } +func (m *Manager) UpdateFormTemplates(ctx context.Context) (UpdateResponse, error) { + if err := os.MkdirAll(m.config.FormsPath, 0o755); err != nil { + return UpdateResponse{}, fmt.Errorf("can't write to forms dir [%w]", err) } log.Printf("Updating form templates; current version is %v", m.getFormsVersion()) - latest, err := m.getLatestFormsInfo() + latest, err := m.getLatestFormsInfo(ctx) if err != nil { return UpdateResponse{}, err } @@ -281,8 +292,7 @@ func (m *Manager) UpdateFormTemplates() (UpdateResponse, error) { }, nil } - err = m.downloadAndUnzipForms(latest.ArchiveURL) - if err != nil { + if err = m.downloadAndUnzipForms(ctx, latest.ArchiveURL); err != nil { return UpdateResponse{}, err } log.Printf("Finished forms update to %v", latest.Version) @@ -298,8 +308,8 @@ type formsInfo struct { ArchiveURL string `json:"archive_url"` } -func (m *Manager) getLatestFormsInfo() (*formsInfo, error) { - resp, err := client.Get(m, formsVersionInfoURL) +func (m *Manager) getLatestFormsInfo(ctx context.Context) (*formsInfo, error) { + resp, err := client.Get(ctx, m.config.UserAgent, formsVersionInfoURL) if err != nil || resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("can't fetch winlink forms version page: %w", err) } @@ -312,9 +322,9 @@ func (m *Manager) getLatestFormsInfo() (*formsInfo, error) { return &v, nil } -func (m *Manager) downloadAndUnzipForms(downloadLink string) error { +func (m *Manager) downloadAndUnzipForms(ctx context.Context, downloadLink string) error { log.Printf("Updating forms via %v", downloadLink) - resp, err := client.Get(m, downloadLink) + resp, err := client.Get(ctx, m.config.UserAgent, downloadLink) if err != nil { return fmt.Errorf("can't download update ZIP: %w", err) } @@ -329,8 +339,7 @@ func (m *Manager) downloadAndUnzipForms(downloadLink string) error { return fmt.Errorf("can't write update ZIP: %w", err) } - unzipDir := m.config.FormsPath - if err := unzip(f.Name(), unzipDir); err != nil { + if err := unzip(f.Name(), m.config.FormsPath); err != nil { return fmt.Errorf("can't unzip forms update: %w", err) } return nil @@ -350,7 +359,7 @@ func unzip(srcArchivePath, dstRoot string) error { } // Ensure target directory exists - if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil { + if err := os.MkdirAll(filepath.Dir(destPath), 0o755); err != nil { return fmt.Errorf("can't create target directory: %w", err) } @@ -481,43 +490,36 @@ func (m *Manager) RenderForm(contentUnsanitized []byte, composeReply bool) (stri return "", err } - retVal, err := m.fillFormTemplate(absPathTemplate, "/api/form?composereply=true&formPath="+formRelPath, regexp.MustCompile(`{var\s+(\w+)\s*}`), formVars) - return retVal, err + return m.fillFormTemplate(absPathTemplate, "/api/form?composereply=true&formPath="+formRelPath, regexp.MustCompile(`{[vV][aA][rR]\s+(\w+)\s*}`), formVars) } // ComposeForm combines all data needed for the whole form-based message: subject, body, and attachment func (m *Manager) ComposeForm(tmplPath string, subject string) (MessageForm, error) { formFolder, err := m.buildFormFolder() if err != nil { - log.Printf("can't build form folder tree %s", err) - return MessageForm{}, err + return MessageForm{}, fmt.Errorf("failed to build form folder tree: %v", err) } tmplPath = filepath.Clean(tmplPath) form, err := findFormFromURI(tmplPath, formFolder) if err != nil { - log.Printf("can't find form to match form %s", tmplPath) - return MessageForm{}, err + return MessageForm{}, fmt.Errorf("failed to find '%s': %v", tmplPath, err) } - varMap := map[string]string{ + formValues := map[string]string{ "subjectline": subject, "templateversion": m.getFormsVersion(), "msgsender": m.config.MyCall, } - - fmt.Printf("Form '%s', version: %s", form.TxtFileURI, varMap["templateversion"]) - + fmt.Printf("Form '%s', version: %s", form.TxtFileURI, formValues["templateversion"]) formMsg, err := formMessageBuilder{ Template: form, - FormValues: varMap, + FormValues: formValues, Interactive: true, IsReply: false, FormsMgr: m, }.build() if err != nil { - err = fmt.Errorf("could not open form file: %w", err) - log.Print(err) return MessageForm{}, err } @@ -526,15 +528,15 @@ func (m *Manager) ComposeForm(tmplPath string, subject string) (MessageForm, err func (f Form) matchesName(nameToMatch string) bool { return f.InitialURI == nameToMatch || - f.InitialURI == nameToMatch+".html" || + strings.EqualFold(f.InitialURI, nameToMatch+htmlFileExt) || f.ViewerURI == nameToMatch || - f.ViewerURI == nameToMatch+".html" || + strings.EqualFold(f.ViewerURI, nameToMatch+htmlFileExt) || f.ReplyInitialURI == nameToMatch || f.ReplyInitialURI == nameToMatch+".0" || f.ReplyViewerURI == nameToMatch || f.ReplyViewerURI == nameToMatch+".0" || f.TxtFileURI == nameToMatch || - f.TxtFileURI == nameToMatch+".txt" + strings.EqualFold(f.TxtFileURI, nameToMatch+txtFileExt) } func (f Form) containsName(partialName string) bool { @@ -564,7 +566,7 @@ func (m *Manager) innerRecursiveBuildFormFolder(rootPath string) (FormFolder, er return FormFolder{}, errors.New(rootPath + " is not a directory") } - retVal := FormFolder{ + folder := FormFolder{ Name: rootFileInfo.Name(), Path: rootFile.Name(), Forms: []Form{}, @@ -573,7 +575,7 @@ func (m *Manager) innerRecursiveBuildFormFolder(rootPath string) (FormFolder, er infos, err := rootFile.Readdir(0) if err != nil { - return retVal, err + return folder, err } _ = rootFile.Close() @@ -582,13 +584,13 @@ func (m *Manager) innerRecursiveBuildFormFolder(rootPath string) (FormFolder, er if info.IsDir() { subfolder, err := m.innerRecursiveBuildFormFolder(path.Join(rootPath, info.Name())) if err != nil { - return retVal, err + return folder, err } - retVal.Folders = append(retVal.Folders, subfolder) - retVal.FormCount += subfolder.FormCount + folder.Folders = append(folder.Folders, subfolder) + folder.FormCount += subfolder.FormCount continue } - if filepath.Ext(info.Name()) != txtFileExt { + if !strings.EqualFold(filepath.Ext(info.Name()), txtFileExt) { continue } frm, err := m.buildFormFromTxt(path.Join(rootPath, info.Name())) @@ -597,17 +599,17 @@ func (m *Manager) innerRecursiveBuildFormFolder(rootPath string) (FormFolder, er } if frm.InitialURI != "" || frm.ViewerURI != "" { formCnt++ - retVal.Forms = append(retVal.Forms, frm) - retVal.FormCount++ + folder.Forms = append(folder.Forms, frm) + folder.FormCount++ } } - sort.Slice(retVal.Folders, func(i, j int) bool { - return retVal.Folders[i].Name < retVal.Folders[j].Name + sort.Slice(folder.Folders, func(i, j int) bool { + return folder.Folders[i].Name < folder.Folders[j].Name }) - sort.Slice(retVal.Forms, func(i, j int) bool { - return retVal.Forms[i].Name < retVal.Forms[j].Name + sort.Slice(folder.Forms, func(i, j int) bool { + return folder.Forms[i].Name < folder.Forms[j].Name }) - return retVal, nil + return folder, nil } func (m *Manager) buildFormFromTxt(txtPath string) (Form, error) { @@ -619,40 +621,41 @@ func (m *Manager) buildFormFromTxt(txtPath string) (Form, error) { formsPathWithSlash := m.config.FormsPath + "/" - retVal := Form{ - Name: strings.TrimSuffix(path.Base(txtPath), ".txt"), + form := Form{ + Name: strings.TrimSuffix(path.Base(txtPath), path.Ext(txtPath)), TxtFileURI: strings.TrimPrefix(txtPath, formsPathWithSlash), } scanner := bufio.NewScanner(f) - baseURI := path.Dir(retVal.TxtFileURI) + baseURI := path.Dir(form.TxtFileURI) for scanner.Scan() { l := scanner.Text() switch { case strings.HasPrefix(l, "Form:"): - trimmed := strings.TrimSpace(strings.TrimPrefix(l, "Form:")) - fileNames := strings.Split(trimmed, ",") - if len(fileNames) >= 2 { - initial := strings.TrimSpace(fileNames[0]) - viewer := strings.TrimSpace(fileNames[1]) - retVal.InitialURI = path.Join(baseURI, initial) - retVal.ViewerURI = path.Join(baseURI, viewer) - } else { - view := strings.TrimSpace(fileNames[0]) - retVal.InitialURI = path.Join(baseURI, view) - retVal.ViewerURI = path.Join(baseURI, view) + // Form: , + files := strings.Split(strings.TrimPrefix(l, "Form:"), ",") + // Extend to absolute paths and add missing html extension + for i := range files { + files[i] = path.Join(baseURI, strings.TrimSpace(files[i])) + if ext := path.Ext(files[i]); ext == "" { + files[i] += ".html" + } + } + form.InitialURI = files[0] + if len(files) > 1 { + form.ViewerURI = files[1] } case strings.HasPrefix(l, "ReplyTemplate:"): - retVal.ReplyTxtFileURI = path.Join(baseURI, strings.TrimSpace(strings.TrimPrefix(l, "ReplyTemplate:"))) - tmpForm, _ := m.buildFormFromTxt(path.Join(m.config.FormsPath, retVal.ReplyTxtFileURI)) - retVal.ReplyInitialURI = tmpForm.InitialURI - retVal.ReplyViewerURI = tmpForm.ViewerURI + form.ReplyTxtFileURI = path.Join(baseURI, strings.TrimSpace(strings.TrimPrefix(l, "ReplyTemplate:"))) + tmpForm, _ := m.buildFormFromTxt(path.Join(m.config.FormsPath, form.ReplyTxtFileURI)) + form.ReplyInitialURI = tmpForm.InitialURI + form.ReplyViewerURI = tmpForm.ViewerURI } } - return retVal, err + return form, err } func findFormFromURI(formName string, folder FormFolder) (Form, error) { - retVal := Form{Name: "unknown"} + form := Form{Name: "unknown"} for _, subFolder := range folder.Folders { form, err := findFormFromURI(formName, subFolder) if err == nil { @@ -673,7 +676,7 @@ func findFormFromURI(formName string, folder FormFolder) (Form, error) { return form, nil } } - return retVal, errors.New("form not found") + return form, errors.New("form not found") } func (m *Manager) findAbsPathForTemplatePath(tmplPath string) (string, error) { @@ -693,15 +696,103 @@ func (m *Manager) findAbsPathForTemplatePath(tmplPath string) (string, error) { return "", errors.New("can't read template folder") } - var retVal string for _, name := range fileNames { if strings.EqualFold(filepath.Base(tmplPath), name) { - retVal = filepath.Join(absPathTemplateFolder, name) - break + return filepath.Join(absPathTemplateFolder, name), nil } } + return "", fmt.Errorf("unable to resolve absolute template path") +} + +// gpsPos returns the current GPS Position +func (m *Manager) gpsPos() (gpsd.Position, error) { + addr := m.config.GPSd.Addr + if addr == "" { + return gpsd.Position{}, errors.New("GPSd: not configured.") + } + if !m.config.GPSd.AllowForms { + return gpsd.Position{}, errors.New("GPSd: allow_forms is disabled. GPS position will not be available in form templates.") + } + + conn, err := gpsd.Dial(addr) + if err != nil { + log.Printf("GPSd daemon: %s", err) + return gpsd.Position{}, err + } + defer conn.Close() - return retVal, nil + conn.Watch(true) + log.Println("Waiting for position from GPSd...") + // TODO: make the GPSd timeout configurable + return conn.NextPosTimeout(3 * time.Second) +} + +type gpsStyle int + +const ( + // documentation: https://www.winlink.org/sites/default/files/RMSE_FORMS/insertion_tags.zip + signedDecimal gpsStyle = iota // 41.1234 -73.4567 + decimal // 46.3795N 121.5835W + degreeMinute // 46-22.77N 121-35.01W +) + +func gpsFmt(style gpsStyle, pos gpsd.Position) string { + var ( + northing string + easting string + latDegrees int + latMinutes float64 + lonDegrees int + lonMinutes float64 + ) + + noPos := gpsd.Position{} + if pos == noPos { + return "(Not available)" + } + switch style { + case degreeMinute: + { + latDegrees = int(math.Trunc(math.Abs(pos.Lat))) + latMinutes = (math.Abs(pos.Lat) - float64(latDegrees)) * 60 + lonDegrees = int(math.Trunc(math.Abs(pos.Lon))) + lonMinutes = (math.Abs(pos.Lon) - float64(lonDegrees)) * 60 + } + fallthrough + case decimal: + { + if pos.Lat >= 0 { + northing = "N" + } else { + northing = "S" + } + if pos.Lon >= 0 { + easting = "E" + } else { + easting = "W" + } + } + } + + switch style { + case signedDecimal: + return fmt.Sprintf("%.4f %.4f", pos.Lat, pos.Lon) + case decimal: + return fmt.Sprintf("%.4f%s %.4f%s", math.Abs(pos.Lat), northing, math.Abs(pos.Lon), easting) + case degreeMinute: + return fmt.Sprintf("%02d-%05.2f%s %03d-%05.2f%s", latDegrees, latMinutes, northing, lonDegrees, lonMinutes, easting) + default: + return "(Not available)" + } +} + +func posToGridSquare(pos gpsd.Position) string { + point := maidenhead.NewPoint(pos.Lat, pos.Lon) + gridsquare, err := point.GridSquare() + if err != nil { + return "" + } + return gridsquare } func (m *Manager) fillFormTemplate(absPathTemplate string, formDestURL string, placeholderRegEx *regexp.Regexp, formVars map[string]string) (string, error) { @@ -723,16 +814,17 @@ func (m *Manager) fillFormTemplate(absPathTemplate string, formDestURL string, p log.Printf("Warning: unsupported string encoding in template %s, expected utf-8", absPathTemplate) } - retVal := "" now := time.Now() - nowDateTime := now.Format("2006-01-02 15:04:05") - nowDateTimeUTC := now.UTC().Format("2006-01-02 15:04:05Z") - nowDate := now.Format("2006-01-02") - nowTime := now.Format("15:04:05") - nowDateUTC := now.UTC().Format("2006-01-02Z") - nowTimeUTC := now.UTC().Format("15:04:05Z") - udtg := strings.ToUpper(now.UTC().Format("021504Z Jan 2006")) + validPos := "NO" + nowPos, err := m.gpsPos() + if err != nil { + debug.Printf("GPSd error: %v", err) + } else { + validPos = "YES" + debug.Printf("GPSd position: %s", gpsFmt(signedDecimal, nowPos)) + } + var buf bytes.Buffer scanner := bufio.NewScanner(bytes.NewReader(sanitizedFileContent)) for scanner.Scan() { l := scanner.Text() @@ -742,19 +834,30 @@ func (m *Manager) fillFormTemplate(absPathTemplate string, formDestURL string, p l = strings.ReplaceAll(l, "{MsgSender}", m.config.MyCall) l = strings.ReplaceAll(l, "{Callsign}", m.config.MyCall) l = strings.ReplaceAll(l, "{ProgramVersion}", "Pat "+m.config.AppVersion) - l = strings.ReplaceAll(l, "{DateTime}", nowDateTime) - l = strings.ReplaceAll(l, "{UDateTime}", nowDateTimeUTC) - l = strings.ReplaceAll(l, "{Date}", nowDate) - l = strings.ReplaceAll(l, "{UDate}", nowDateUTC) - l = strings.ReplaceAll(l, "{UDTG}", udtg) - l = strings.ReplaceAll(l, "{Time}", nowTime) - l = strings.ReplaceAll(l, "{UTime}", nowTimeUTC) + l = strings.ReplaceAll(l, "{DateTime}", formatDateTime(now)) + l = strings.ReplaceAll(l, "{UDateTime}", formatDateTimeUTC(now)) + l = strings.ReplaceAll(l, "{Date}", formatDate(now)) + l = strings.ReplaceAll(l, "{UDate}", formatDateUTC(now)) + l = strings.ReplaceAll(l, "{UDTG}", formatUDTG(now)) + l = strings.ReplaceAll(l, "{Time}", formatTime(now)) + l = strings.ReplaceAll(l, "{UTime}", formatTimeUTC(now)) + l = strings.ReplaceAll(l, "{GPS}", gpsFmt(degreeMinute, nowPos)) + l = strings.ReplaceAll(l, "{GPS_DECIMAL}", gpsFmt(decimal, nowPos)) + l = strings.ReplaceAll(l, "{GPS_SIGNED_DECIMAL}", gpsFmt(signedDecimal, nowPos)) + // Lots of undocumented tags found in the Winlink check in form. + // Note also various ways of capitalizing. Perhaps best to do case insenstive string replacements.... + l = strings.ReplaceAll(l, "{Latitude}", fmt.Sprintf("%.4f", nowPos.Lat)) + l = strings.ReplaceAll(l, "{latitude}", fmt.Sprintf("%.4f", nowPos.Lat)) + l = strings.ReplaceAll(l, "{Longitude}", fmt.Sprintf("%.4f", nowPos.Lon)) + l = strings.ReplaceAll(l, "{longitude}", fmt.Sprintf("%.4f", nowPos.Lon)) + l = strings.ReplaceAll(l, "{GridSquare}", posToGridSquare(nowPos)) + l = strings.ReplaceAll(l, "{GPSValid}", fmt.Sprintf("%s ", validPos)) if placeholderRegEx != nil { l = fillPlaceholders(l, placeholderRegEx, formVars) } - retVal += l + "\n" + buf.WriteString(l + "\n") } - return retVal, nil + return buf.String(), nil } func (m *Manager) getFormsVersion() string { @@ -814,11 +917,6 @@ func (b formMessageBuilder) build() (MessageForm, error) { tmplPath = filepath.Join(b.FormsMgr.config.FormsPath, b.Template.ReplyTxtFileURI) } - retVal, err := b.scanTmplBuildMessage(tmplPath) - if err != nil { - return MessageForm{}, err - } - b.initFormValues() formVarsAsXML := "" @@ -839,7 +937,14 @@ func (b formMessageBuilder) build() (MessageForm, error) { replier = filepath.Base(b.Template.ReplyTxtFileURI) } - retVal.AttachmentXML = fmt.Sprintf(`%s + msgForm, err := b.scanTmplBuildMessage(tmplPath) + if err != nil { + return MessageForm{}, err + } + + // Add XML if a viewer is defined for this form + if b.Template.ViewerURI != "" { + msgForm.AttachmentXML = fmt.Sprintf(`%s %s %s @@ -854,19 +959,23 @@ func (b formMessageBuilder) build() (MessageForm, error) { `, - xml.Header, - "1.0", - b.FormsMgr.config.AppVersion, - time.Now().UTC().Format("20060102150405"), - b.FormsMgr.config.MyCall, - b.FormsMgr.config.Locator, - viewer, - replier, - formVarsAsXML) - retVal.AttachmentName = b.FormsMgr.GetXMLAttachmentNameForForm(b.Template, false) - retVal.Subject = strings.TrimSpace(retVal.Subject) - retVal.Body = strings.TrimSpace(retVal.Body) - return retVal, nil + xml.Header, + "1.0", + b.FormsMgr.config.AppVersion, + time.Now().UTC().Format("20060102150405"), + b.FormsMgr.config.MyCall, + b.FormsMgr.config.Locator, + viewer, + replier, + formVarsAsXML) + msgForm.AttachmentName = b.FormsMgr.GetXMLAttachmentNameForForm(b.Template, false) + } + + msgForm.To = strings.TrimSpace(msgForm.To) + msgForm.Cc = strings.TrimSpace(msgForm.Cc) + msgForm.Subject = strings.TrimSpace(msgForm.Subject) + msgForm.Body = strings.TrimSpace(msgForm.Body) + return msgForm, nil } func (b formMessageBuilder) initFormValues() { @@ -899,18 +1008,23 @@ func (b formMessageBuilder) scanTmplBuildMessage(tmplPath string) (MessageForm, placeholderRegEx := regexp.MustCompile(`<[vV][aA][rR]\s+(\w+)\s*>`) scanner := bufio.NewScanner(infile) - var retVal MessageForm + var msgForm MessageForm + var inBody bool for scanner.Scan() { lineTmpl := scanner.Text() lineTmpl = fillPlaceholders(lineTmpl, placeholderRegEx, b.FormValues) lineTmpl = strings.ReplaceAll(lineTmpl, "", b.FormsMgr.config.MyCall) lineTmpl = strings.ReplaceAll(lineTmpl, "", "Pat "+b.FormsMgr.config.AppVersion) - if strings.HasPrefix(lineTmpl, "Form:") || - strings.HasPrefix(lineTmpl, "ReplyTemplate:") || - strings.HasPrefix(lineTmpl, "To:") || - strings.HasPrefix(lineTmpl, "Msg:") { + if strings.HasPrefix(lineTmpl, "Form:") { continue } + if strings.HasPrefix(lineTmpl, "ReplyTemplate:") { + continue + } + if strings.HasPrefix(lineTmpl, "Msg:") { + lineTmpl = strings.TrimSpace(strings.TrimPrefix(lineTmpl, "Msg:")) + inBody = true + } if b.Interactive { matches := placeholderRegEx.FindAllStringSubmatch(lineTmpl, -1) fmt.Println(lineTmpl) @@ -930,14 +1044,20 @@ func (b formMessageBuilder) scanTmplBuildMessage(tmplPath string) (MessageForm, } lineTmpl = fillPlaceholders(lineTmpl, placeholderRegEx, b.FormValues) - if strings.HasPrefix(lineTmpl, "Subject:") { - retVal.Subject = strings.TrimPrefix(lineTmpl, "Subject:") - } else { - retVal.Body += lineTmpl + "\n" + switch { + case strings.HasPrefix(lineTmpl, "Subject:"): + msgForm.Subject = strings.TrimPrefix(lineTmpl, "Subject:") + case strings.HasPrefix(lineTmpl, "To:"): + msgForm.To = strings.TrimPrefix(lineTmpl, "To:") + case strings.HasPrefix(lineTmpl, "Cc:"): + msgForm.Cc = strings.TrimPrefix(lineTmpl, "Cc:") + case inBody: + msgForm.Body += lineTmpl + "\n" + default: + log.Printf("skipping unknown template line: '%s'", lineTmpl) } } - - return retVal, nil + return msgForm, nil } func xmlEscape(s string) string { @@ -965,6 +1085,7 @@ func fillPlaceholders(s string, re *regexp.Regexp, values map[string]string) str func (m *Manager) cleanupOldFormData() { m.postedFormData.Lock() + defer m.postedFormData.Unlock() for key, form := range m.postedFormData.internalFormDataMap { elapsed := time.Since(form.Submitted).Hours() if elapsed > 24 { @@ -972,7 +1093,6 @@ func (m *Manager) cleanupOldFormData() { delete(m.postedFormData.internalFormDataMap, key) } } - m.postedFormData.Unlock() } func (m *Manager) isNewerVersion(newestVersion string) bool { @@ -997,12 +1117,12 @@ func (m *Manager) isNewerVersion(newestVersion string) bool { type httpClient struct{ http.Client } -func (c httpClient) Get(m *Manager, url string) (*http.Response, error) { - req, err := http.NewRequest("GET", url, nil) +func (c httpClient) Get(ctx context.Context, userAgent, url string) (*http.Response, error) { + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, err } - req.Header.Set("User-Agent", m.config.UserAgent) + req.Header.Set("User-Agent", userAgent) req.Header.Set("Cache-Control", "no-cache") return c.Do(req) } diff --git a/internal/gpsd/gpsd.go b/internal/gpsd/gpsd.go index a133c26b..294931ca 100644 --- a/internal/gpsd/gpsd.go +++ b/internal/gpsd/gpsd.go @@ -127,8 +127,8 @@ func (c *Conn) Close() error { // Next returns the next object sent from the daemon, or an error. // // The empty interface returned can be any of the following types: -// * Sky: A Sky object reports a sky view of the GPS satellite positions. -// * TPV: A TPV object is a time-position-velocity report. +// - Sky: A Sky object reports a sky view of the GPS satellite positions. +// - TPV: A TPV object is a time-position-velocity report. func (c *Conn) Next() (interface{}, error) { c.mu.Lock() defer c.mu.Unlock() diff --git a/internal/osutil/rlimit_freebsd.go b/internal/osutil/rlimit_freebsd.go index 33c4d10b..e66b843d 100644 --- a/internal/osutil/rlimit_freebsd.go +++ b/internal/osutil/rlimit_freebsd.go @@ -1,3 +1,4 @@ +//go:build freebsd // +build freebsd package osutil diff --git a/internal/osutil/rlimit_unix.go b/internal/osutil/rlimit_unix.go index 161e744a..c8fb0753 100644 --- a/internal/osutil/rlimit_unix.go +++ b/internal/osutil/rlimit_unix.go @@ -1,3 +1,4 @@ +//go:build !windows && !freebsd // +build !windows,!freebsd package osutil diff --git a/internal/osutil/rlimit_windows.go b/internal/osutil/rlimit_windows.go index cf67536a..956e5118 100644 --- a/internal/osutil/rlimit_windows.go +++ b/internal/osutil/rlimit_windows.go @@ -1,3 +1,4 @@ +//go:build windows // +build windows package osutil diff --git a/listen.go b/listen.go index d613198f..677d754f 100644 --- a/listen.go +++ b/listen.go @@ -5,6 +5,7 @@ package main import ( + "context" "log" "net" "strings" @@ -29,16 +30,25 @@ func Unlisten(param string) { func Listen(listenStr string) { methods := strings.FieldsFunc(listenStr, SplitFunc) for _, method := range methods { + // Rewrite the generic ax25:// scheme to use a specified AX.25 engine. + if method == MethodAX25 { + method = defaultAX25Method() + } + switch strings.ToLower(method) { - case MethodWinmor: - listenHub.Enable(WINMORListener{}) case MethodArdop: listenHub.Enable(ARDOPListener{}) case MethodTelnet: listenHub.Enable(TelnetListener{}) - case MethodAX25: - listenHub.Enable(&AX25Listener{}) - case MethodSerialTNC: + case MethodAX25AGWPE: + listenHub.Enable(&AX25AGWPEListener{}) + case MethodAX25Linux: + listenHub.Enable(&AX25LinuxListener{}) + case MethodVaraFM: + listenHub.Enable(VaraFMListener{}) + case MethodVaraHF: + listenHub.Enable(VaraHFListener{}) + case MethodAX25SerialTNC, MethodSerialTNCDeprecated: log.Printf("%s listen not implemented, ignoring.", method) default: log.Printf("'%s' is not a valid listen method", method) @@ -48,53 +58,38 @@ func Listen(listenStr string) { log.Printf("Listening for incoming traffic on %s...", listenStr) } -type AX25Listener struct{ stopBeacon chan<- struct{} } +type AX25LinuxListener struct{ stopBeacon func() } -func (l *AX25Listener) Init() (net.Listener, error) { - return ax25.ListenAX25(config.AX25.Port, fOptions.MyCall) +func (l *AX25LinuxListener) Init() (net.Listener, error) { + return ax25.ListenAX25(config.AX25Linux.Port, fOptions.MyCall) } -func (l *AX25Listener) BeaconStart() error { - if config.AX25.Beacon.Every > 0 { - l.stopBeacon = l.beaconLoop(time.Duration(config.AX25.Beacon.Every) * time.Second) +func (l *AX25LinuxListener) BeaconStart() error { + interval := time.Duration(config.AX25.Beacon.Every) * time.Second + if interval == 0 { + return nil + } + b, err := ax25.NewAX25Beacon(config.AX25Linux.Port, fOptions.MyCall, config.AX25.Beacon.Destination, config.AX25.Beacon.Message) + if err != nil { + return err } + l.stopBeacon = doEvery(interval, func() { + if err := b.Now(); err != nil { + log.Printf("%s beacon failed: %s", l.Name(), err) + l.stopBeacon() + } + }) return nil } -func (l *AX25Listener) BeaconStop() { - select { - case l.stopBeacon <- struct{}{}: - default: +func (l *AX25LinuxListener) BeaconStop() { + if l.stopBeacon != nil { + l.stopBeacon() } } -func (l *AX25Listener) beaconLoop(dur time.Duration) chan<- struct{} { - stop := make(chan struct{}, 1) - go func() { - b, err := ax25.NewAX25Beacon(config.AX25.Port, fOptions.MyCall, config.AX25.Beacon.Destination, config.AX25.Beacon.Message) - if err != nil { - log.Printf("Unable to activate beacon: %s", err) - return - } - - t := time.Tick(dur) - for { - select { - case <-t: - if err := b.Now(); err != nil { - log.Printf("%s beacon failed: %s", l.Name(), err) - return - } - case <-stop: - return - } - } - }() - return stop -} - -func (l *AX25Listener) CurrentFreq() (Frequency, bool) { return 0, false } -func (l *AX25Listener) Name() string { return MethodAX25 } +func (l *AX25LinuxListener) CurrentFreq() (Frequency, bool) { return 0, false } +func (l *AX25LinuxListener) Name() string { return MethodAX25Linux } type ARDOPListener struct{} @@ -124,26 +119,103 @@ func (l ARDOPListener) BeaconStart() error { func (l ARDOPListener) BeaconStop() { adTNC.BeaconEvery(0) } -type WINMORListener struct{} +type VaraFMListener struct{} -func (l WINMORListener) Name() string { return MethodWinmor } -func (l WINMORListener) Init() (net.Listener, error) { - if err := initWinmorTNC(); err != nil { +func (l VaraFMListener) Name() string { return MethodVaraFM } +func (l VaraFMListener) Init() (net.Listener, error) { + if err := initVaraFMModem(); err != nil { + return nil, err + } + ln, err := varaFMModem.Listen() + if err != nil { return nil, err } - return wmTNC.Listen(config.Winmor.InboundBandwidth) + return ln, err } -func (l WINMORListener) CurrentFreq() (Frequency, bool) { - if rig, ok := rigs[config.Winmor.Rig]; ok { +func (l VaraFMListener) CurrentFreq() (Frequency, bool) { + if rig, ok := rigs[config.VaraFM.Rig]; ok { f, _ := rig.GetFreq() return Frequency(f), ok } return 0, false } +type VaraHFListener struct{} + +func (l VaraHFListener) Name() string { return MethodVaraHF } +func (l VaraHFListener) Init() (net.Listener, error) { + if err := initVaraHFModem(); err != nil { + return nil, err + } + ln, err := varaHFModem.Listen() + if err != nil { + return nil, err + } + return ln, err +} + +func (l VaraHFListener) CurrentFreq() (Frequency, bool) { + if rig, ok := rigs[config.VaraHF.Rig]; ok { + f, _ := rig.GetFreq() + return Frequency(f), ok + } + return 0, false +} + +type AX25AGWPEListener struct{ stopBeacon func() } + +func (l *AX25AGWPEListener) Name() string { return MethodAX25AGWPE } + +func (l *AX25AGWPEListener) Init() (net.Listener, error) { + if err := initAGWPE(); err != nil { + return nil, err + } + return agwpeTNC.Listen() +} + +func (l *AX25AGWPEListener) CurrentFreq() (Frequency, bool) { return 0, false } + +func (l *AX25AGWPEListener) BeaconStart() error { + b := config.AX25.Beacon + interval := time.Duration(b.Every) * time.Second + l.stopBeacon = doEvery(interval, func() { + if err := agwpeTNC.SendUI([]byte(b.Message), b.Destination); err != nil { + log.Printf("%s beacon failed: %s", l.Name(), err) + l.stopBeacon() + } + }) + return nil +} + +func (l AX25AGWPEListener) BeaconStop() { + if l.stopBeacon != nil { + l.stopBeacon() + } +} + type TelnetListener struct{} func (l TelnetListener) Name() string { return MethodTelnet } func (l TelnetListener) Init() (net.Listener, error) { return telnet.Listen(config.Telnet.ListenAddr) } func (l TelnetListener) CurrentFreq() (Frequency, bool) { return 0, false } + +func doEvery(interval time.Duration, fn func()) (cancel func()) { + if interval == 0 { + return + } + ctx, cancel := context.WithCancel(context.Background()) + go func() { + t := time.NewTicker(interval) + defer t.Stop() + for { + select { + case <-ctx.Done(): + return + case <-t.C: + fn() + } + } + }() + return cancel +} diff --git a/listener_hub.go b/listener_hub.go index 0cc4f8aa..bf7b274b 100644 --- a/listener_hub.go +++ b/listener_hub.go @@ -169,6 +169,9 @@ func (h *ListenerHub) Enable(t TransportListener) { } func (h *ListenerHub) Disable(name string) (bool, error) { + if name == MethodAX25 { + name = defaultAX25Method() + } h.mu.Lock() defer func() { h.mu.Unlock() diff --git a/main.go b/main.go index d3bd5625..73b506a7 100644 --- a/main.go +++ b/main.go @@ -6,12 +6,14 @@ package main import ( + "context" "fmt" "io" "log" "net" "os" "os/exec" + "os/signal" "path/filepath" "runtime" "strconv" @@ -33,12 +35,19 @@ import ( ) const ( - MethodWinmor = "winmor" - MethodArdop = "ardop" - MethodTelnet = "telnet" - MethodAX25 = "ax25" - MethodSerialTNC = "serial-tnc" - MethodPactor = "pactor" + MethodArdop = "ardop" + MethodTelnet = "telnet" + MethodPactor = "pactor" + MethodVaraHF = "varahf" + MethodVaraFM = "varafm" + + MethodAX25 = "ax25" + MethodAX25AGWPE = MethodAX25 + "+agwpe" + MethodAX25Linux = MethodAX25 + "+linux" + MethodAX25SerialTNC = MethodAX25 + "+serial-tnc" + + // TODO: Remove after some release cycles (2023-05-21) + MethodSerialTNCDeprecated = "serial-tnc" ) var commands = []Command{ @@ -73,16 +82,17 @@ var commands = []Command{ LongLived: true, }, { - Str: "compose", - Desc: "Compose a new message.\n" + + Str: "compose", + Desc: "Compose a new message.", + Usage: "[options]\n" + "\tIf no options are passed, composes interactively.\n" + "\tIf options are passed, reads message from stdin similar to mail(1).", - Usage: "[options]", Options: map[string]string{ - "--callsign, -r": "Callsign to send from. Default reads from config", + "--from, -r": "Address to send from. Default is your call from config or --mycall, but can be specified to use tactical addresses.", "--subject, -s": "Subject", "--attachment , -a": "Attachment path (may be repeated)", "--cc, -c": "CC Address(es) (may be repeated)", + "--p2p-only": "Send over peer to peer links only (avoid CMS)", "": "Recipient address (may be repeated)", }, HandleFunc: composeMessage, @@ -90,8 +100,8 @@ var commands = []Command{ { Str: "read", Desc: "Read messages.", - HandleFunc: func(args []string) { - readMail() + HandleFunc: func(ctx context.Context, args []string) { + readMail(ctx) }, }, { @@ -137,8 +147,8 @@ var commands = []Command{ { Str: "updateforms", Desc: "Download the latest form templates from winlink.org.", - HandleFunc: func(args []string) { - if _, err := formsMgr.UpdateFormTemplates(); err != nil { + HandleFunc: func(ctx context.Context, args []string) { + if _, err := formsMgr.UpdateFormTemplates(ctx); err != nil { log.Printf("%v", err) } }, @@ -151,7 +161,7 @@ var commands = []Command{ { Str: "version", Desc: "Print the application version.", - HandleFunc: func(args []string) { + HandleFunc: func(_ context.Context, args []string) { fmt.Printf("%s %s\n", buildinfo.AppName, buildinfo.VersionString()) }, }, @@ -200,10 +210,9 @@ func optionsSet() *pflag.FlagSet { set := pflag.NewFlagSet("options", pflag.ExitOnError) set.StringVar(&fOptions.MyCall, "mycall", "", "Your callsign (winlink user).") - set.StringVarP(&fOptions.Listen, "listen", "l", "", "Comma-separated list of methods to listen on (e.g. winmor,ardop,telnet,ax25).") + set.StringVarP(&fOptions.Listen, "listen", "l", "", "Comma-separated list of methods to listen on (e.g. ardop,telnet,ax25).") set.BoolVarP(&fOptions.SendOnly, "send-only", "s", false, "Download inbound messages later, send only.") set.BoolVarP(&fOptions.RadioOnly, "radio-only", "", false, "Radio Only mode (Winlink Hybrid RMS only).") - set.BoolVarP(&fOptions.Robust, "robust", "r", false, "Use robust modes only (useful to improve s/n-ratio at remote winmor station).") set.BoolVar(&fOptions.IgnoreBusy, "ignore-busy", false, "Don't wait for clear channel before connecting to a node.") defaultMBox := filepath.Join(directories.DataDir(), "mailbox") @@ -257,13 +266,33 @@ func main() { debug.Printf("Event log file is\t'%s'", fOptions.EventLogPath) directories.MigrateLegacyDataDir() + // Graceful shutdown by cancelling background context on interrupt. + // + // If we have an active connection, cancel that instead. + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go func() { + dirtyDisconnectNext := false // So we can do a dirty disconnect on the second interrupt + for { + <-sig + if ok := abortActiveConnection(dirtyDisconnectNext); ok { + dirtyDisconnectNext = !dirtyDisconnectNext + } else { + break + } + } + cancel() + }() + // Skip initialization for some commands switch cmd.Str { case "help": helpHandle(args) return case "configure", "version": - cmd.HandleFunc(args) + cmd.HandleFunc(ctx, args) return } @@ -278,7 +307,6 @@ func main() { if err != nil { log.Fatalf("Unable to load/write config: %s", err) } - maybeUpdateFormsDir(os.Args) // Initialize logger f, err := os.Create(fOptions.LogPath) @@ -325,6 +353,7 @@ func main() { AppVersion: buildinfo.VersionStringShort(), UserAgent: buildinfo.UserAgent(), LineReader: readLine, + GPSd: config.GPSd, }) // Make sure we clean up on exit, closing any open resources etc. @@ -357,24 +386,10 @@ func main() { } // Start command execution - cmd.HandleFunc(args) -} - -func maybeUpdateFormsDir(args []string) { - // Backward compatability for config file forms_path - formsFlagExplicitlyUsed := false - for i := 0; i < len(args); i++ { - if strings.HasPrefix(args[i], "--forms") { - formsFlagExplicitlyUsed = true - } - } - if !formsFlagExplicitlyUsed && config.FormsPath != "" { - debug.Printf("Updating forms dir based on config file: %v", config.FormsPath) - fOptions.FormsPath = config.FormsPath - } + cmd.HandleFunc(ctx, args) } -func configureHandle(args []string) { +func configureHandle(ctx context.Context, args []string) { // Ensure config file has been written _, err := ReadConfig(fOptions.ConfigPath) if os.IsNotExist(err) { @@ -384,14 +399,14 @@ func configureHandle(args []string) { } } - cmd := exec.Command(EditorName(), fOptions.ConfigPath) + cmd := exec.CommandContext(ctx, EditorName(), fOptions.ConfigPath) cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr if err := cmd.Run(); err != nil { log.Fatalf("Unable to start editor: %s", err) } } -func InteractiveHandle(args []string) { +func InteractiveHandle(ctx context.Context, args []string) { var http string set := pflag.NewFlagSet("interactive", pflag.ExitOnError) set.StringVar(&http, "http", "", "HTTP listen address") @@ -399,20 +414,22 @@ func InteractiveHandle(args []string) { set.Parse(args) if http == "" { - Interactive() + Interactive(ctx) return } + ctx, cancel := context.WithCancel(ctx) + defer cancel() go func() { - if err := ListenAndServe(http); err != nil { + if err := ListenAndServe(ctx, http); err != nil { log.Println(err) } }() time.Sleep(time.Second) - Interactive() + Interactive(ctx) } -func httpHandle(args []string) { +func httpHandle(ctx context.Context, args []string) { addr := config.HTTPAddr if addr == "" { addr = ":8080" // For backwards compatibility (remove in future) @@ -429,12 +446,12 @@ func httpHandle(args []string) { promptHub.OmitTerminal(true) - if err := ListenAndServe(addr); err != nil { - log.Fatal(err) + if err := ListenAndServe(ctx, addr); err != nil { + log.Println(err) } } -func connectHandle(args []string) { +func connectHandle(_ context.Context, args []string) { if args[0] == "" { fmt.Println("Missing argument, try 'connect help'.") } @@ -461,26 +478,39 @@ func helpHandle(args []string) { } func cleanup() { - listenHub.Close() - - if wmTNC != nil { - if err := wmTNC.Close(); err != nil { - log.Fatalf("Failure to close winmor TNC: %s", err) - } - } + debug.Printf("Starting cleanup") + defer debug.Printf("Cleanup done") + abortActiveConnection(false) + listenHub.Close() if adTNC != nil { if err := adTNC.Close(); err != nil { - log.Fatalf("Failure to close ardop TNC: %s", err) + log.Printf("Failure to close ardop TNC: %s", err) } } - if pModem != nil { if err := pModem.Close(); err != nil { - log.Fatalf("Failure to close pactor modem: %s", err) + log.Printf("Failure to close pactor modem: %s", err) } } + if varaFMModem != nil { + if err := varaFMModem.Close(); err != nil { + log.Printf("Failure to close varafm modem: %s", err) + } + } + + if varaHFModem != nil { + if err := varaHFModem.Close(); err != nil { + log.Printf("Failure to close varahf modem: %s", err) + } + } + + if agwpeTNC != nil { + if err := agwpeTNC.Close(); err != nil { + log.Printf("Failure to close AGWPE TNC: %s", err) + } + } eventLog.Close() } @@ -504,6 +534,9 @@ func loadHamlibRigs() map[string]hamlib.VFO { log.Printf("Missing address-field for rig '%s', skipping.", name) continue } + if conf.Network == "" { + conf.Network = "tcp" + } rig, err := hamlib.Open(conf.Network, conf.Address) if err != nil { @@ -541,7 +574,7 @@ func loadHamlibRigs() map[string]hamlib.VFO { return rigs } -func extractMessageHandle(args []string) { +func extractMessageHandle(_ context.Context, args []string) { if len(args) == 0 || args[0] == "" { panic("TODO: usage") } @@ -581,7 +614,7 @@ func EditorName() string { return "vi" } -func posReportHandle(args []string) { +func posReportHandle(ctx context.Context, args []string) { var latlon, comment string set := pflag.NewFlagSet("position", pflag.ExitOnError) @@ -614,15 +647,28 @@ func posReportHandle(args []string) { log.Fatalf("GPSd daemon: %s", err) } defer conn.Close() - conn.Watch(true) + posChan := make(chan gpsd.Position) + go func() { + defer close(posChan) + pos, err := conn.NextPos() + if err != nil { + log.Printf("GPSd: %s", err) + return + } + posChan <- pos + }() + log.Println("Waiting for position from GPSd...") // TODO: Spinning bar? - pos, err := conn.NextPos() - if err != nil { - log.Fatalf("GPSd: %s", err) + var pos gpsd.Position + select { + case p := <-posChan: + pos = p + case <-ctx.Done(): + log.Println("Cancelled") + return } - report.Lat = &pos.Lat report.Lon = &pos.Lon if config.GPSd.UseServerTime { diff --git a/make.bash b/make.bash index cc429c63..8f973294 100755 --- a/make.bash +++ b/make.bash @@ -4,13 +4,14 @@ set -e export GO111MODULE=on if [ -d $GOOS ]; then OS=$(go env GOOS); else OS=$GOOS; fi +if [ -d $CGO_ENABLED ]; then CGO_ENABLED=$(go env CGO_ENABLED); else OS=$CGO_ENABLED; fi GITREV=$(git rev-parse --short HEAD) VERSION=$(grep "Version =" internal/buildinfo/VERSION.go|cut -d '"' -f2) -# Go 1.16 or later is required +# Go 1.19 or later is required GO_POINT_VERSION=$(go version| perl -ne 'm/go1\.(\d+)/; print $1;') -[ "$GO_POINT_VERSION" -lt "16" ] && echo "Go 1.16 or later required" && exit 1; +[ "$GO_POINT_VERSION" -lt "19" ] && echo "Go 1.19 or later required" && exit 1; AX25VERSION="0.0.12-rc4" AX25DIST="libax25-${AX25VERSION}" @@ -20,11 +21,22 @@ function install_libax25 { [[ -f "${AX25DIST}" ]] || curl -LSsf "${AX25DIST_URL}" | tar zx cd "${AX25DIST}/" && ./configure --prefix=/ && make && cd ../../ } +function build_web { + cd web + if [ -d $NVM_DIR ]; then + source $NVM_DIR/nvm.sh + nvm install + nvm use + fi + npm install + npm run production +} [[ "$1" == "libax25" ]] && install_libax25 && exit 0; +[[ "$1" == "web" ]] && build_web && exit 0; # Link against libax25 (statically) on Linux -if [[ "$OS" == "linux"* ]]; then +if [[ "$OS" == "linux"* ]] && [[ "$CGO_ENABLED" == "1" ]]; then TAGS="libax25 $TAGS" LIB=".build/${AX25DIST}/.libs/libax25.a" if [[ -z "$CGO_LDFLAGS" ]] && [[ -f "$LIB" ]]; then @@ -37,10 +49,13 @@ if [[ "$OS" == "linux"* ]]; then echo " this issue, set CGO_LDFLAGS to the full path of" echo " libax25.a, or run 'make.bash libax25' to download" echo " and compile ${AX25DIST} in .build/" - sleep 3; else TAGS="static $TAGS" fi +else + if [[ "$OS" == "linux"* ]]; then + echo "WARNING: CGO unavailable. libax25 (ax25+linux) will not be supported with this build." + fi fi echo -e "Downloading Go dependencies..." @@ -57,11 +72,12 @@ echo echo "Building Pat v$VERSION..." go build -tags "$TAGS" -ldflags "-X \"github.com/la5nta/pat/internal/buildinfo.GitRev=$GITREV\"" $(go list .) -# Build macOS pkg (amd64) +# Build macOS pkg if [[ "$OS" == "darwin"* ]] && command -v packagesbuild >/dev/null 2>&1; then + ARCH=$(go env GOARCH) echo "Generating macOS installer package..." packagesbuild osx/pat.pkgproj - mv 'Pat :: A Modern Winlink Client.pkg' "pat_${VERSION}_darwin_amd64_unsigned.pkg" + mv 'Pat :: A Modern Winlink Client.pkg' "pat_${VERSION}_darwin_${ARCH}_unsigned.pkg" fi echo -e "Enjoy!" diff --git a/man/pat-configure.1 b/man/pat-configure.1 index c7d32d25..c4067cc7 100644 --- a/man/pat-configure.1 +++ b/man/pat-configure.1 @@ -3,7 +3,7 @@ pat configure \- opens Pat's configuration file using the system default editor .SH Configuration .SS Main Configuration -The default configuration file for pat is \fI~/.wl2k/config.json\fP +The current configuration file is located in the \fIPAT_CONFIG_PATH\fP returned from \fIpat env\fP .sp 1 To get "on the air" you'll first have to set up your callsign, maidenhead locator, and secure login credentials. Look for the attributes \fImycall\fP, \fIlocator\fP and \fIsecure_login_password\fP and set them appropriately. .sp 1 diff --git a/man/pat.1 b/man/pat.1 index 7b67e26c..8099727f 100644 --- a/man/pat.1 +++ b/man/pat.1 @@ -43,22 +43,22 @@ Print detailed help for a given command. .SS Options .TP \fR--config string\fP -Path to config file (default "/home/USER/.wl2k/config.json"). +Path to config file (located in the \fIPAT_CONFIG_PATH\fP returned from \fIpat env\fP). .TP \fR--event-log string\fP -Path to event log file (default "/home/USER/.wl2k/config.json"). +Path to event log file (located in the \fIPAT_CONFIG_PATH\fP returned from \fIpat env\fP). .TP \fR--ignore-busy\fP Don't wait for clear channel before connevting to a node. .TP \fR-l, --listen string\fP -Comma-separated list of methods to listen on (e.g. winmor,ardop,telnet,ax25). +Comma-separated list of methods to listen on (e.g. ardop,telnet,ax25). .TP \fR--log string\fP -Path to log file. The file is truncated on each startup (default "/home/USER/.wl2k/pat.log"). +Path to log file. The file is truncated on each startup (located in the \fIPAT_LOG_PATH\fP returned from \fIpat env\fP). .TP \fR--mbox string\fP -Path to mailbox directory (default "/home/USER/.wl2k/mailbox"). +Path to mailbox directory (located in the \fIPAT_MAILBOX_PATH\fP returned from \fIpat env\fP). .TP \fR--mycall string\fP Your callsing (winlink user). @@ -66,9 +66,6 @@ Your callsing (winlink user). \fR--radio-only\fP Radio Only mode (Winlink Hybrid RMS only). .TP -\fR-r, --robust\fP -Use robust modes only (Useful to improve s/n ratio at remote winmor station). -.TP \fR-s, --send-only\fP Download inbound messages later, send only. .SH "See Also" diff --git a/osx/pat.pkgproj b/osx/pat.pkgproj index 4af23755..b5a2a24e 100644 --- a/osx/pat.pkgproj +++ b/osx/pat.pkgproj @@ -588,7 +588,7 @@ USE_HFS+_COMPRESSION VERSION - 0.12.1 + 0.15.1 UUID 5562F199-B4C6-48BC-98FA-5BBA494CB61F diff --git a/prompt_hub.go b/prompt_hub.go index ca6ace47..053f75fa 100644 --- a/prompt_hub.go +++ b/prompt_hub.go @@ -2,7 +2,6 @@ package main import ( "fmt" - "io" "os" "time" @@ -83,29 +82,6 @@ func (p *PromptHub) Prompt(kind, message string) <-chan PromptResponse { return prompt.resp } -type ReadAborter struct { - *os.File - abort chan struct{} -} - -func (r ReadAborter) Read(p []byte) (int, error) { - tick := time.Tick(100 * time.Millisecond) - for { - select { - case <-r.abort: - return 0, io.EOF - case <-tick: - stat, err := r.Stat() - if err != nil { - panic(err) - } - if stat.Size() > 0 { - return r.File.Read(p) - } - } - } -} - func (p *PromptHub) promptTerminal(prompt Prompt) { switch prompt.Kind { case "password": diff --git a/read.go b/read.go index 0458977a..3400e004 100644 --- a/read.go +++ b/read.go @@ -6,6 +6,7 @@ package main import ( "bytes" + "context" "fmt" "io" "log" @@ -23,14 +24,14 @@ import ( var mailboxes = []string{"in", "out", "sent", "archive"} -func readMail() { +func readMail(ctx context.Context) { w := os.Stdout for { // Query user for mailbox to list printMailboxes(w) fmt.Fprintf(w, "\nChoose mailbox [n]: ") - mailboxIdx, ok := readInt() + mailboxIdx, ok := readInt(ctx) if !ok { break } else if mailboxIdx+1 > len(mailboxes) { @@ -54,7 +55,7 @@ func readMail() { // Query user for message to print fmt.Fprintf(w, "Choose message [n]: ") - msgIdx, ok := readInt() + msgIdx, ok := readInt(ctx) if !ok { break } else if msgIdx+1 > len(msgs) { @@ -82,14 +83,19 @@ func readMail() { } } -func readInt() (int, bool) { - str := readLine() - if str == "" { +func readInt(ctx context.Context) (int, bool) { + cs := make(chan string, 1) + go func() { cs <- readLine() }() + select { + case <-ctx.Done(): return 0, false + case str := <-cs: + if str == "" { + return 0, false + } + i, _ := strconv.Atoi(str) + return i, true } - - i, _ := strconv.Atoi(str) - return i, true } type PrettyAddrSlice []fbb.Address diff --git a/riglist.go b/riglist.go index 43708bb7..744f55be 100644 --- a/riglist.go +++ b/riglist.go @@ -2,11 +2,13 @@ // Use of this source code is governed by the MIT-license that can be // found in the LICENSE file. +//go:build libhamlib // +build libhamlib package main import ( + "context" "fmt" "strings" @@ -24,7 +26,10 @@ func init() { commands = append(commands[:8], append([]Command{cmd}, commands[8:]...)...) } -func riglistHandle(args []string) { +func riglistHandle(ctx context.Context, args []string) { + if args[0] == "" { + fmt.Println("Missing argument") + } term := strings.ToLower(args[0]) fmt.Print("id\ttransceiver\n") diff --git a/rmslist.go b/rmslist.go index 6cbfb290..ff2caf7f 100644 --- a/rmslist.go +++ b/rmslist.go @@ -5,6 +5,7 @@ package main import ( + "context" "encoding/json" "fmt" "log" @@ -49,6 +50,12 @@ type RMS struct { } func (r RMS) IsMode(mode string) bool { + if mode == MethodVaraFM { + return strings.HasPrefix(r.Modes, "VARA FM") + } + if mode == MethodVaraHF { + return strings.HasPrefix(r.Modes, "VARA") && !strings.HasPrefix(r.Modes, "VARA FM") + } return strings.Contains(strings.ToLower(r.Modes), mode) } @@ -62,7 +69,7 @@ func (r byDist) Len() int { return len(r) } func (r byDist) Swap(i, j int) { r[i], r[j] = r[j], r[i] } func (r byDist) Less(i, j int) bool { return r[i].Distance < r[j].Distance } -func rmsListHandle(args []string) { +func rmsListHandle(ctx context.Context, args []string) { set := pflag.NewFlagSet("rmslist", pflag.ExitOnError) mode := set.StringP("mode", "m", "", "") band := set.StringP("band", "b", "", "") @@ -76,7 +83,7 @@ func rmsListHandle(args []string) { } *mode = strings.ToLower(*mode) - rList, err := ReadRMSList(*forceDownload, func(rms RMS) bool { + rList, err := ReadRMSList(ctx, *forceDownload, func(rms RMS) bool { switch { case query != "" && !strings.HasPrefix(rms.Callsign, query): return false @@ -113,7 +120,7 @@ func rmsListHandle(args []string) { } } -func ReadRMSList(forceDownload bool, filterFn func(rms RMS) (keep bool)) ([]RMS, error) { +func ReadRMSList(ctx context.Context, forceDownload bool, filterFn func(rms RMS) (keep bool)) ([]RMS, error) { me, err := maidenhead.ParseLocator(config.Locator) if err != nil { log.Print("Missing or Invalid Locator, will not compute distance and Azimuth") @@ -127,7 +134,7 @@ func ReadRMSList(forceDownload bool, filterFn func(rms RMS) (keep bool)) ([]RMS, filePath := filepath.Join(directories.DataDir(), fileName+".json") debug.Printf("RMS list file is %s", filePath) - f, err := cmsapi.GetGatewayStatusCached(filePath, forceDownload, config.ServiceCodes...) + f, err := cmsapi.GetGatewayStatusCached(ctx, filePath, forceDownload, config.ServiceCodes...) if err != nil { return nil, err } @@ -137,7 +144,7 @@ func ReadRMSList(forceDownload bool, filterFn func(rms RMS) (keep bool)) ([]RMS, return nil, err } - var slice []RMS + var slice = []RMS{} for _, gw := range status.Gateways { for _, channel := range gw.Channels { r := RMS{ @@ -164,13 +171,44 @@ func ReadRMSList(forceDownload bool, filterFn func(rms RMS) (keep bool)) ([]RMS, return slice, nil } -func toURL(gc cmsapi.GatewayChannel, targetcall string) *url.URL { +func toURL(gc cmsapi.GatewayChannel, targetCall string) *url.URL { freq := Frequency(gc.Frequency).Dial(gc.SupportedModes) - chURL, _ := url.Parse(fmt.Sprintf("%s:///%s?freq=%v", toTransport(gc), targetcall, freq.KHz())) + chURL, _ := url.Parse(fmt.Sprintf("%s:///%s?freq=%v", toTransport(gc), targetCall, freq.KHz())) + addBandwidth(gc, chURL) return chURL } -var transports = []string{MethodWinmor, MethodAX25, MethodPactor, MethodArdop} +func addBandwidth(gc cmsapi.GatewayChannel, chURL *url.URL) { + bw := "" + modeF := strings.Fields(gc.SupportedModes) + if modeF[0] == "ARDOP" { + if len(modeF) > 1 { + bw = modeF[1] + "MAX" + } + } else if modeF[0] == "VARA" { + if len(modeF) > 1 && modeF[1] == "FM" { + // VARA FM should not set bandwidth in connect URL or sent over the command port, + // it's set in the VARA Setup dialog + bw = "" + } else { + // VARA HF may be 500, 2750, or none which is implicitly 2300 + if len(modeF) > 1 { + if len(modeF) > 1 { + bw = modeF[1] + } + } else { + bw = "2300" + } + } + } + if bw != "" { + v := chURL.Query() + v.Set("bw", bw) + chURL.RawQuery = v.Encode() + } +} + +var transports = []string{MethodAX25, MethodPactor, MethodArdop, MethodVaraFM, MethodVaraHF} func toTransport(gc cmsapi.GatewayChannel) string { modes := strings.ToLower(gc.SupportedModes) @@ -179,6 +217,12 @@ func toTransport(gc cmsapi.GatewayChannel) string { // bug(maritnhpedersen): We really don't know which transport to use here. It could be serial-tnc or ax25, but ax25 is most likely. return MethodAX25 } + if strings.HasPrefix(modes, "vara fm") { + return MethodVaraFM + } + if strings.HasPrefix(modes, "vara") { + return MethodVaraHF + } if strings.Contains(modes, transport) { return transport } diff --git a/rmslist_test.go b/rmslist_test.go new file mode 100644 index 00000000..ffc2feb0 --- /dev/null +++ b/rmslist_test.go @@ -0,0 +1,180 @@ +package main + +import ( + "github.com/la5nta/pat/internal/cmsapi" + "net/url" + "reflect" + "testing" +) + +func Test_toURL(t *testing.T) { + type args struct { + channel cmsapi.GatewayChannel + targetCall string + } + tests := []struct { + name string + args args + want *url.URL + }{ + { + name: "ax25 1200", + args: args{ + channel: cmsapi.GatewayChannel{ + Frequency: 145050000, + SupportedModes: "Packet 1200", + }, + targetCall: "K0NTS-10", + }, + want: parseUrl("ax25:///K0NTS-10?freq=145050"), + }, + { + name: "ax25 9600", + args: args{ + channel: cmsapi.GatewayChannel{ + Frequency: 438075000, + SupportedModes: "Packet 9600", + }, + targetCall: "HB9AK-14", + }, + want: parseUrl("ax25:///HB9AK-14?freq=438075"), + }, + { + name: "adrop 2000", + args: args{ + channel: cmsapi.GatewayChannel{ + Frequency: 3586500, + SupportedModes: "ARDOP 2000", + }, + targetCall: "K0SI", + }, + want: parseUrl("ardop:///K0SI?bw=2000MAX&freq=3585"), + }, + { + name: "adrop 500", + args: args{ + channel: cmsapi.GatewayChannel{ + Frequency: 3584000, + SupportedModes: "ARDOP 500", + }, + targetCall: "F1ZWL", + }, + want: parseUrl("ardop:///F1ZWL?bw=500MAX&freq=3582.5"), + }, + { + // These are quite rare but are seen in the wild + name: "adrop 1000", + args: args{ + channel: cmsapi.GatewayChannel{ + Frequency: 3588000, + SupportedModes: "ARDOP 1000", + }, + targetCall: "N3HYM-10", + }, + want: parseUrl("ardop:///N3HYM-10?bw=1000MAX&freq=3586.5"), + }, + { + // This is a notional ARDOP station that doesn't specify bandwidth in supportedModes. + // None appear today in the RMS list, but maybe they could. + name: "adrop unspec", + args: args{ + channel: cmsapi.GatewayChannel{ + Frequency: 7584000, + SupportedModes: "ARDOP", + }, + targetCall: "T3ST", + }, + want: parseUrl("ardop:///T3ST?freq=7582.5"), + }, + { + name: "pactor", + args: args{ + channel: cmsapi.GatewayChannel{ + Frequency: 1850000, + SupportedModes: "Pactor 1,2", + }, + targetCall: "K1EHZ", + }, + want: parseUrl("pactor:///K1EHZ?freq=1848.5"), + }, + { + name: "robust packet", + args: args{ + channel: cmsapi.GatewayChannel{ + Frequency: 7099400, + SupportedModes: "Robust Packet", + }, + targetCall: "N3HYM-10", + }, + want: parseUrl("ax25:///N3HYM-10?freq=7099.4"), + }, + { + name: "vara hf 500", + args: args{ + channel: cmsapi.GatewayChannel{ + Frequency: 7064000, + SupportedModes: "VARA 500", + }, + targetCall: "W0VG", + }, + want: parseUrl("varahf:///W0VG?bw=500&freq=7062.5"), + }, + { + name: "vara hf unspec", + args: args{ + channel: cmsapi.GatewayChannel{ + Frequency: 7103000, + SupportedModes: "VARA", + }, + targetCall: "W0VG", + }, + want: parseUrl("varahf:///W0VG?bw=2300&freq=7101.5"), + }, + { + name: "vara hf 2750", + args: args{ + channel: cmsapi.GatewayChannel{ + Frequency: 3597900, + SupportedModes: "VARA 2750", + }, + targetCall: "W1EO", + }, + want: parseUrl("varahf:///W1EO?bw=2750&freq=3596.4"), + }, + { + name: "vara fm narrow", + args: args{ + channel: cmsapi.GatewayChannel{ + Frequency: 145070000, + SupportedModes: "VARA FM", + }, + targetCall: "W0TQ", + }, + // vara transport adapter will default to narrow + want: parseUrl("varafm:///W0TQ?freq=145070"), + }, + { + name: "vara fm wide", + args: args{ + channel: cmsapi.GatewayChannel{ + Frequency: 145510000, + SupportedModes: "VARA FM WIDE", + }, + targetCall: "W1AW-10", + }, + want: parseUrl("varafm:///W1AW-10?freq=145510"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := toURL(tt.args.channel, tt.args.targetCall); !reflect.DeepEqual(got, tt.want) { + t.Errorf("toURL() = %v, want %v", got, tt.want) + } + }) + } +} + +func parseUrl(str string) *url.URL { + parse, _ := url.Parse(str) + return parse +} diff --git a/usage.go b/usage.go index 04ba202d..67da54d7 100644 --- a/usage.go +++ b/usage.go @@ -8,19 +8,22 @@ var ( UsageConnect = `'alias' or 'transport://[host][/digi]/targetcall[?params...]' transport: - winmor: WINMOR TNC - ardop: ARDOP TNC - ax25: AX.25 (Linux only) - telnet: TCP/IP - serial-tnc: Serial AX.25 TNC - pactor: SCS PTC modems + telnet: TCP/IP + ardop: ARDOP TNC + pactor: SCS PTC modems + varahf: VARA HF TNC + varafm: VARA FM TNC + ax25: AX.25 (Default - uses engine specified in config) + ax25+agwpe: AX.25 (AGWPE/Direwolf) + ax25+linux: AX.25 (Linux kernel) + ax25+serial-tnc: AX.25 (Serial TNC) host: Used to address the host interface (TNC/modem), _not_ to be confused with the connection PATH. Format: [user[:pass]@]host[:port] telnet: [user:pass]@host:port - ax25: (optional) host=axport + ax25+linux: (optional) host=axport pactor: (optional) serial device (e.g. COM1 or /dev/ttyUSB0) path: @@ -28,20 +31,19 @@ path: multiple hops (e.g. AX.25), they are separated by '/'. params: - ?freq= Sets QSY frequency (winmor, ardop and ax25 only) + ?freq= Sets QSY frequency (ardop and ax25 only) ?host= Overrides the host part of the path. Useful for serial-tnc to specify e.g. /dev/ttyS0. ` ExampleConnect = ` - connect telnet (alias) Connect to one of the Winlink Common Message Servers via tcp. - connect ax25:///LA1B-10 Connect to the RMS Gateway LA1B-10 using Linux AX.25 on the default axport. - connect ax25://tmd710/LA1B-10 Connect to the RMS Gateway LA1B-10 using Linux AX.25 on axport 'tmd710'. - connect ax25:///LA1B/LA5NTA Peer-to-peer connection with LA5NTA via LA1B digipeater. - connect winmor:///LA3F Connect to the RMS HF Gateway LA3F using WINMOR TNC on default tcp address and port. - connect winmor:///LA3F?freq=5350 Same as above, but set dial frequency of the radio using rigcontrol. - connect ardop:///LA3F Connect to the RMS HF Gateway LA3F using ARDOP on the default tcp address and port. - connect ardop:///LA3F?freq=5350 Same as above, but set dial frequency of the radio using rigcontrol. - connect serial-tnc:///LA1B-10 Connect to the RMS Gateway LA1B-10 over a AX.25 serial TNC on the default serial port. - connect pactor:///LA3F Connect to RMS HF Gateway LA3F using PACTOR. + connect telnet (alias) Connect to one of the Winlink Common Message Servers via tcp. + connect ax25:///LA1B-10 Connect to the RMS Gateway LA1B-10 using AX.25 engine as per configuration. + connect ax25+linux://tmd710/LA1B-10 Connect to LA1B-10 using Linux kernel's AX.25 stack on axport 'tmd710'. + connect ax25:///LA1B/LA5NTA Peer-to-peer connection with LA5NTA via LA1B digipeater. + connect ardop:///LA3F Connect to the RMS HF Gateway LA3F using ARDOP on the default tcp address and port. + connect ardop:///LA3F?freq=5350 Same as above, but set dial frequency of the radio using rigcontrol. + connect pactor:///LA3F Connect to RMS HF Gateway LA3F using PACTOR. + connect varahf:///LA1B Connect to RMS HF Gateway LA1B using VARA HF TNC. + connect varafm:///LA5NTA Connect to LA5NTA using VARA FM TNC. ` ) diff --git a/web/.nvmrc b/web/.nvmrc new file mode 100644 index 00000000..c32828c2 --- /dev/null +++ b/web/.nvmrc @@ -0,0 +1 @@ +lts/hydrogen \ No newline at end of file diff --git a/web/.prettierignore b/web/.prettierignore new file mode 100644 index 00000000..de4d1f00 --- /dev/null +++ b/web/.prettierignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/web/dist/css/style.css b/web/dist/css/style.css new file mode 100644 index 00000000..e62c2df2 --- /dev/null +++ b/web/dist/css/style.css @@ -0,0 +1,16 @@ +/*! + * Bootstrap v3.2.0 (http://getbootstrap.com) + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + *//*! normalize.css v3.0.1 | MIT License | git.io/normalize */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background:0 0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}@media print{*{color:#000!important;text-shadow:none!important;background:transparent!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff!important}.navbar{display:none}.table td,.table th{background-color:#fff!important}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:Glyphicons Halflings;src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format("embedded-opentype"),url(../fonts/glyphicons-halflings-regular.woff) format("woff"),url(../fonts/glyphicons-halflings-regular.ttf) format("truetype"),url("\"data:image/svg+xml,%3C?xml version='1.0' standalone='no'?%3E %3C!DOCTYPE svg PUBLIC '-/W3C/DTD SVG 1.1/EN' 'http:/www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' %3E %3Csvg xmlns='http:/www.w3.org/2000/svg'%3E %3Cmetadata%3E%3C/metadata%3E %3Cdefs%3E %3Cfont id='glyphicons_halflingsregular' horiz-adv-x='1200' %3E %3Cfont-face units-per-em='1200' ascent='960' descent='-240' /%3E %3Cmissing-glyph horiz-adv-x='500' /%3E %3Cglyph /%3E %3Cglyph /%3E %3Cglyph unicode='&%23xd;' /%3E %3Cglyph unicode=' ' /%3E %3Cglyph unicode='*' d='M100 500v200h259l-183 183l141 141l183 -183v259h200v-259l183 183l141 -141l-183 -183h259v-200h-259l183 -183l-141 -141l-183 183v-259h-200v259l-183 -183l-141 141l183 183h-259z' /%3E %3Cglyph unicode='+' d='M0 400v300h400v400h300v-400h400v-300h-400v-400h-300v400h-400z' /%3E %3Cglyph unicode='&%23xa0;' /%3E %3Cglyph unicode='&%23x2000;' horiz-adv-x='652' /%3E %3Cglyph unicode='&%23x2001;' horiz-adv-x='1304' /%3E %3Cglyph unicode='&%23x2002;' horiz-adv-x='652' /%3E %3Cglyph unicode='&%23x2003;' horiz-adv-x='1304' /%3E %3Cglyph unicode='&%23x2004;' horiz-adv-x='434' /%3E %3Cglyph unicode='&%23x2005;' horiz-adv-x='326' /%3E %3Cglyph unicode='&%23x2006;' horiz-adv-x='217' /%3E %3Cglyph unicode='&%23x2007;' horiz-adv-x='217' /%3E %3Cglyph unicode='&%23x2008;' horiz-adv-x='163' /%3E %3Cglyph unicode='&%23x2009;' horiz-adv-x='260' /%3E %3Cglyph unicode='&%23x200a;' horiz-adv-x='72' /%3E %3Cglyph unicode='&%23x202f;' horiz-adv-x='260' /%3E %3Cglyph unicode='&%23x205f;' horiz-adv-x='326' /%3E %3Cglyph unicode='&%23x20ac;' d='M100 500l100 100h113q0 47 5 100h-218l100 100h135q37 167 112 257q117 141 297 141q242 0 354 -189q60 -103 66 -209h-181q0 55 -25.5 99t-63.5 68t-75 36.5t-67 12.5q-24 0 -52.5 -10t-62.5 -32t-65.5 -67t-50.5 -107h379l-100 -100h-300q-6 -46 -6 -100h406l-100 -100 h-300q9 -74 33 -132t52.5 -91t62 -54.5t59 -29t46.5 -7.5q29 0 66 13t75 37t63.5 67.5t25.5 96.5h174q-31 -172 -128 -278q-107 -117 -274 -117q-205 0 -324 158q-36 46 -69 131.5t-45 205.5h-217z' /%3E %3Cglyph unicode='&%23x2212;' d='M200 400h900v300h-900v-300z' /%3E %3Cglyph unicode='&%23x25fc;' horiz-adv-x='500' d='M0 0z' /%3E %3Cglyph unicode='&%23x2601;' d='M-14 494q0 -80 56.5 -137t135.5 -57h750q120 0 205 86.5t85 207.5t-85 207t-205 86q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5z' /%3E %3Cglyph unicode='&%23x2709;' d='M0 100l400 400l200 -200l200 200l400 -400h-1200zM0 300v600l300 -300zM0 1100l600 -603l600 603h-1200zM900 600l300 300v-600z' /%3E %3Cglyph unicode='&%23x270f;' d='M-13 -13l333 112l-223 223zM187 403l214 -214l614 614l-214 214zM887 1103l214 -214l99 92q13 13 13 32.5t-13 33.5l-153 153q-15 13 -33 13t-33 -13z' /%3E %3Cglyph unicode='&%23xe001;' d='M0 1200h1200l-500 -550v-550h300v-100h-800v100h300v550z' /%3E %3Cglyph unicode='&%23xe002;' d='M14 84q18 -55 86 -75.5t147 5.5q65 21 109 69t44 90v606l600 155v-521q-64 16 -138 -7q-79 -26 -122.5 -83t-25.5 -111q18 -55 86 -75.5t147 4.5q70 23 111.5 63.5t41.5 95.5v881q0 10 -7 15.5t-17 2.5l-752 -193q-10 -3 -17 -12.5t-7 -19.5v-689q-64 17 -138 -7 q-79 -25 -122.5 -82t-25.5 -112z' /%3E %3Cglyph unicode='&%23xe003;' d='M23 693q0 200 142 342t342 142t342 -142t142 -342q0 -142 -78 -261l300 -300q7 -8 7 -18t-7 -18l-109 -109q-8 -7 -18 -7t-18 7l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 693q0 -136 97 -233t234 -97t233.5 96.5t96.5 233.5t-96.5 233.5t-233.5 96.5 t-234 -97t-97 -233z' /%3E %3Cglyph unicode='&%23xe005;' d='M100 784q0 64 28 123t73 100.5t104.5 64t119 20.5t120 -38.5t104.5 -104.5q48 69 109.5 105t121.5 38t118.5 -20.5t102.5 -64t71 -100.5t27 -123q0 -57 -33.5 -117.5t-94 -124.5t-126.5 -127.5t-150 -152.5t-146 -174q-62 85 -145.5 174t-149.5 152.5t-126.5 127.5 t-94 124.5t-33.5 117.5z' /%3E %3Cglyph unicode='&%23xe006;' d='M-72 800h479l146 400h2l146 -400h472l-382 -278l145 -449l-384 275l-382 -275l146 447zM168 71l2 1z' /%3E %3Cglyph unicode='&%23xe007;' d='M-72 800h479l146 400h2l146 -400h472l-382 -278l145 -449l-384 275l-382 -275l146 447zM168 71l2 1zM237 700l196 -142l-73 -226l192 140l195 -141l-74 229l193 140h-235l-77 211l-78 -211h-239z' /%3E %3Cglyph unicode='&%23xe008;' d='M0 0v143l400 257v100q-37 0 -68.5 74.5t-31.5 125.5v200q0 124 88 212t212 88t212 -88t88 -212v-200q0 -51 -31.5 -125.5t-68.5 -74.5v-100l400 -257v-143h-1200z' /%3E %3Cglyph unicode='&%23xe009;' d='M0 0v1100h1200v-1100h-1200zM100 100h100v100h-100v-100zM100 300h100v100h-100v-100zM100 500h100v100h-100v-100zM100 700h100v100h-100v-100zM100 900h100v100h-100v-100zM300 100h600v400h-600v-400zM300 600h600v400h-600v-400zM1000 100h100v100h-100v-100z M1000 300h100v100h-100v-100zM1000 500h100v100h-100v-100zM1000 700h100v100h-100v-100zM1000 900h100v100h-100v-100z' /%3E %3Cglyph unicode='&%23xe010;' d='M0 50v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5zM0 650v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400 q-21 0 -35.5 14.5t-14.5 35.5zM600 50v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5zM600 650v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400 q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5z' /%3E %3Cglyph unicode='&%23xe011;' d='M0 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM0 450v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200 q-21 0 -35.5 14.5t-14.5 35.5zM0 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5 t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 450v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5 v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 450v200q0 21 14.5 35.5t35.5 14.5h200 q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5z' /%3E %3Cglyph unicode='&%23xe012;' d='M0 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM0 450q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v200q0 21 -14.5 35.5t-35.5 14.5h-200q-21 0 -35.5 -14.5 t-14.5 -35.5v-200zM0 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 50v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5 t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5zM400 450v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5zM400 850v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5 v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5z' /%3E %3Cglyph unicode='&%23xe013;' d='M29 454l419 -420l818 820l-212 212l-607 -607l-206 207z' /%3E %3Cglyph unicode='&%23xe014;' d='M106 318l282 282l-282 282l212 212l282 -282l282 282l212 -212l-282 -282l282 -282l-212 -212l-282 282l-282 -282z' /%3E %3Cglyph unicode='&%23xe015;' d='M23 693q0 200 142 342t342 142t342 -142t142 -342q0 -142 -78 -261l300 -300q7 -8 7 -18t-7 -18l-109 -109q-8 -7 -18 -7t-18 7l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 693q0 -136 97 -233t234 -97t233.5 96.5t96.5 233.5t-96.5 233.5t-233.5 96.5 t-234 -97t-97 -233zM300 600v200h100v100h200v-100h100v-200h-100v-100h-200v100h-100z' /%3E %3Cglyph unicode='&%23xe016;' d='M23 694q0 200 142 342t342 142t342 -142t142 -342q0 -141 -78 -262l300 -299q7 -7 7 -18t-7 -18l-109 -109q-8 -8 -18 -8t-18 8l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 694q0 -136 97 -233t234 -97t233.5 97t96.5 233t-96.5 233t-233.5 97t-234 -97 t-97 -233zM300 601h400v200h-400v-200z' /%3E %3Cglyph unicode='&%23xe017;' d='M23 600q0 183 105 331t272 210v-166q-103 -55 -165 -155t-62 -220q0 -177 125 -302t302 -125t302 125t125 302q0 120 -62 220t-165 155v166q167 -62 272 -210t105 -331q0 -118 -45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5 zM500 750q0 -21 14.5 -35.5t35.5 -14.5h100q21 0 35.5 14.5t14.5 35.5v400q0 21 -14.5 35.5t-35.5 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-400z' /%3E %3Cglyph unicode='&%23xe018;' d='M100 1h200v300h-200v-300zM400 1v500h200v-500h-200zM700 1v800h200v-800h-200zM1000 1v1200h200v-1200h-200z' /%3E %3Cglyph unicode='&%23xe019;' d='M26 601q0 -33 6 -74l151 -38l2 -6q14 -49 38 -93l3 -5l-80 -134q45 -59 105 -105l133 81l5 -3q45 -26 94 -39l5 -2l38 -151q40 -5 74 -5q27 0 74 5l38 151l6 2q46 13 93 39l5 3l134 -81q56 44 104 105l-80 134l3 5q24 44 39 93l1 6l152 38q5 40 5 74q0 28 -5 73l-152 38 l-1 6q-16 51 -39 93l-3 5l80 134q-44 58 -104 105l-134 -81l-5 3q-45 25 -93 39l-6 1l-38 152q-40 5 -74 5q-27 0 -74 -5l-38 -152l-5 -1q-50 -14 -94 -39l-5 -3l-133 81q-59 -47 -105 -105l80 -134l-3 -5q-25 -47 -38 -93l-2 -6l-151 -38q-6 -48 -6 -73zM385 601 q0 88 63 151t152 63t152 -63t63 -151q0 -89 -63 -152t-152 -63t-152 63t-63 152z' /%3E %3Cglyph unicode='&%23xe020;' d='M100 1025v50q0 10 7.5 17.5t17.5 7.5h275v100q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5v-100h275q10 0 17.5 -7.5t7.5 -17.5v-50q0 -11 -7 -18t-18 -7h-1050q-11 0 -18 7t-7 18zM200 100v800h900v-800q0 -41 -29.5 -71t-70.5 -30h-700q-41 0 -70.5 30 t-29.5 71zM300 100h100v700h-100v-700zM500 100h100v700h-100v-700zM500 1100h300v100h-300v-100zM700 100h100v700h-100v-700zM900 100h100v700h-100v-700z' /%3E %3Cglyph unicode='&%23xe021;' d='M1 601l656 644l644 -644h-200v-600h-300v400h-300v-400h-300v600h-200z' /%3E %3Cglyph unicode='&%23xe022;' d='M100 25v1150q0 11 7 18t18 7h475v-500h400v-675q0 -11 -7 -18t-18 -7h-850q-11 0 -18 7t-7 18zM700 800v300l300 -300h-300z' /%3E %3Cglyph unicode='&%23xe023;' d='M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM500 500v400h100 v-300h200v-100h-300z' /%3E %3Cglyph unicode='&%23xe024;' d='M-100 0l431 1200h209l-21 -300h162l-20 300h208l431 -1200h-538l-41 400h-242l-40 -400h-539zM488 500h224l-27 300h-170z' /%3E %3Cglyph unicode='&%23xe025;' d='M0 0v400h490l-290 300h200v500h300v-500h200l-290 -300h490v-400h-1100zM813 200h175v100h-175v-100z' /%3E %3Cglyph unicode='&%23xe026;' d='M1 600q0 122 47.5 233t127.5 191t191 127.5t233 47.5t233 -47.5t191 -127.5t127.5 -191t47.5 -233t-47.5 -233t-127.5 -191t-191 -127.5t-233 -47.5t-233 47.5t-191 127.5t-127.5 191t-47.5 233zM188 600q0 -170 121 -291t291 -121t291 121t121 291t-121 291t-291 121 t-291 -121t-121 -291zM350 600h150v300h200v-300h150l-250 -300z' /%3E %3Cglyph unicode='&%23xe027;' d='M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM350 600l250 300 l250 -300h-150v-300h-200v300h-150z' /%3E %3Cglyph unicode='&%23xe028;' d='M0 25v475l200 700h800l199 -700l1 -475q0 -11 -7 -18t-18 -7h-1150q-11 0 -18 7t-7 18zM200 500h200l50 -200h300l50 200h200l-97 500h-606z' /%3E %3Cglyph unicode='&%23xe029;' d='M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -172 121.5 -293t292.5 -121t292.5 121t121.5 293q0 171 -121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM500 397v401 l297 -200z' /%3E %3Cglyph unicode='&%23xe030;' d='M23 600q0 -118 45.5 -224.5t123 -184t184 -123t224.5 -45.5t224.5 45.5t184 123t123 184t45.5 224.5h-150q0 -177 -125 -302t-302 -125t-302 125t-125 302t125 302t302 125q136 0 246 -81l-146 -146h400v400l-145 -145q-157 122 -355 122q-118 0 -224.5 -45.5t-184 -123 t-123 -184t-45.5 -224.5z' /%3E %3Cglyph unicode='&%23xe031;' d='M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5q198 0 355 -122l145 145v-400h-400l147 147q-112 80 -247 80q-177 0 -302 -125t-125 -302h-150zM100 0v400h400l-147 -147q112 -80 247 -80q177 0 302 125t125 302h150q0 -118 -45.5 -224.5t-123 -184t-184 -123 t-224.5 -45.5q-198 0 -355 122z' /%3E %3Cglyph unicode='&%23xe032;' d='M100 0h1100v1200h-1100v-1200zM200 100v900h900v-900h-900zM300 200v100h100v-100h-100zM300 400v100h100v-100h-100zM300 600v100h100v-100h-100zM300 800v100h100v-100h-100zM500 200h500v100h-500v-100zM500 400v100h500v-100h-500zM500 600v100h500v-100h-500z M500 800v100h500v-100h-500z' /%3E %3Cglyph unicode='&%23xe033;' d='M0 100v600q0 41 29.5 70.5t70.5 29.5h100v200q0 82 59 141t141 59h300q82 0 141 -59t59 -141v-200h100q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-900q-41 0 -70.5 29.5t-29.5 70.5zM400 800h300v150q0 21 -14.5 35.5t-35.5 14.5h-200 q-21 0 -35.5 -14.5t-14.5 -35.5v-150z' /%3E %3Cglyph unicode='&%23xe034;' d='M100 0v1100h100v-1100h-100zM300 400q60 60 127.5 84t127.5 17.5t122 -23t119 -30t110 -11t103 42t91 120.5v500q-40 -81 -101.5 -115.5t-127.5 -29.5t-138 25t-139.5 40t-125.5 25t-103 -29.5t-65 -115.5v-500z' /%3E %3Cglyph unicode='&%23xe035;' d='M0 275q0 -11 7 -18t18 -7h50q11 0 18 7t7 18v300q0 127 70.5 231.5t184.5 161.5t245 57t245 -57t184.5 -161.5t70.5 -231.5v-300q0 -11 7 -18t18 -7h50q11 0 18 7t7 18v300q0 116 -49.5 227t-131 192.5t-192.5 131t-227 49.5t-227 -49.5t-192.5 -131t-131 -192.5 t-49.5 -227v-300zM200 20v460q0 8 6 14t14 6h160q8 0 14 -6t6 -14v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14zM800 20v460q0 8 6 14t14 6h160q8 0 14 -6t6 -14v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14z' /%3E %3Cglyph unicode='&%23xe036;' d='M0 400h300l300 -200v800l-300 -200h-300v-400zM688 459l141 141l-141 141l71 71l141 -141l141 141l71 -71l-141 -141l141 -141l-71 -71l-141 141l-141 -141z' /%3E %3Cglyph unicode='&%23xe037;' d='M0 400h300l300 -200v800l-300 -200h-300v-400zM700 857l69 53q111 -135 111 -310q0 -169 -106 -302l-67 54q86 110 86 248q0 146 -93 257z' /%3E %3Cglyph unicode='&%23xe038;' d='M0 401v400h300l300 200v-800l-300 200h-300zM702 858l69 53q111 -135 111 -310q0 -170 -106 -303l-67 55q86 110 86 248q0 145 -93 257zM889 951l7 -8q123 -151 123 -344q0 -189 -119 -339l-7 -8l81 -66l6 8q142 178 142 405q0 230 -144 408l-6 8z' /%3E %3Cglyph unicode='&%23xe039;' d='M0 0h500v500h-200v100h-100v-100h-200v-500zM0 600h100v100h400v100h100v100h-100v300h-500v-600zM100 100v300h300v-300h-300zM100 800v300h300v-300h-300zM200 200v100h100v-100h-100zM200 900h100v100h-100v-100zM500 500v100h300v-300h200v-100h-100v-100h-200v100 h-100v100h100v200h-200zM600 0v100h100v-100h-100zM600 1000h100v-300h200v-300h300v200h-200v100h200v500h-600v-200zM800 800v300h300v-300h-300zM900 0v100h300v-100h-300zM900 900v100h100v-100h-100zM1100 200v100h100v-100h-100z' /%3E %3Cglyph unicode='&%23xe040;' d='M0 200h100v1000h-100v-1000zM100 0v100h300v-100h-300zM200 200v1000h100v-1000h-100zM500 0v91h100v-91h-100zM500 200v1000h200v-1000h-200zM700 0v91h100v-91h-100zM800 200v1000h100v-1000h-100zM900 0v91h200v-91h-200zM1000 200v1000h200v-1000h-200z' /%3E %3Cglyph unicode='&%23xe041;' d='M0 700l1 475q0 10 7.5 17.5t17.5 7.5h474l700 -700l-500 -500zM148 953q0 -42 29 -71q30 -30 71.5 -30t71.5 30q29 29 29 71t-29 71q-30 30 -71.5 30t-71.5 -30q-29 -29 -29 -71z' /%3E %3Cglyph unicode='&%23xe042;' d='M1 700l1 475q0 11 7 18t18 7h474l700 -700l-500 -500zM148 953q0 -42 30 -71q29 -30 71 -30t71 30q30 29 30 71t-30 71q-29 30 -71 30t-71 -30q-30 -29 -30 -71zM701 1200h100l700 -700l-500 -500l-50 50l450 450z' /%3E %3Cglyph unicode='&%23xe043;' d='M100 0v1025l175 175h925v-1000l-100 -100v1000h-750l-100 -100h750v-1000h-900z' /%3E %3Cglyph unicode='&%23xe044;' d='M200 0l450 444l450 -443v1150q0 20 -14.5 35t-35.5 15h-800q-21 0 -35.5 -15t-14.5 -35v-1151z' /%3E %3Cglyph unicode='&%23xe045;' d='M0 100v700h200l100 -200h600l100 200h200v-700h-200v200h-800v-200h-200zM253 829l40 -124h592l62 124l-94 346q-2 11 -10 18t-18 7h-450q-10 0 -18 -7t-10 -18zM281 24l38 152q2 10 11.5 17t19.5 7h500q10 0 19.5 -7t11.5 -17l38 -152q2 -10 -3.5 -17t-15.5 -7h-600 q-10 0 -15.5 7t-3.5 17z' /%3E %3Cglyph unicode='&%23xe046;' d='M0 200q0 -41 29.5 -70.5t70.5 -29.5h1000q41 0 70.5 29.5t29.5 70.5v600q0 41 -29.5 70.5t-70.5 29.5h-150q-4 8 -11.5 21.5t-33 48t-53 61t-69 48t-83.5 21.5h-200q-41 0 -82 -20.5t-70 -50t-52 -59t-34 -50.5l-12 -20h-150q-41 0 -70.5 -29.5t-29.5 -70.5v-600z M356 500q0 100 72 172t172 72t172 -72t72 -172t-72 -172t-172 -72t-172 72t-72 172zM494 500q0 -44 31 -75t75 -31t75 31t31 75t-31 75t-75 31t-75 -31t-31 -75zM900 700v100h100v-100h-100z' /%3E %3Cglyph unicode='&%23xe047;' d='M53 0h365v66q-41 0 -72 11t-49 38t1 71l92 234h391l82 -222q16 -45 -5.5 -88.5t-74.5 -43.5v-66h417v66q-34 1 -74 43q-18 19 -33 42t-21 37l-6 13l-385 998h-93l-399 -1006q-24 -48 -52 -75q-12 -12 -33 -25t-36 -20l-15 -7v-66zM416 521l178 457l46 -140l116 -317h-340 z' /%3E %3Cglyph unicode='&%23xe048;' d='M100 0v89q41 7 70.5 32.5t29.5 65.5v827q0 28 -1 39.5t-5.5 26t-15.5 21t-29 14t-49 14.5v71l471 -1q120 0 213 -88t93 -228q0 -55 -11.5 -101.5t-28 -74t-33.5 -47.5t-28 -28l-12 -7q8 -3 21.5 -9t48 -31.5t60.5 -58t47.5 -91.5t21.5 -129q0 -84 -59 -156.5t-142 -111 t-162 -38.5h-500zM400 200h161q89 0 153 48.5t64 132.5q0 90 -62.5 154.5t-156.5 64.5h-159v-400zM400 700h139q76 0 130 61.5t54 138.5q0 82 -84 130.5t-239 48.5v-379z' /%3E %3Cglyph unicode='&%23xe049;' d='M200 0v57q77 7 134.5 40.5t65.5 80.5l173 849q10 56 -10 74t-91 37q-6 1 -10.5 2.5t-9.5 2.5v57h425l2 -57q-33 -8 -62 -25.5t-46 -37t-29.5 -38t-17.5 -30.5l-5 -12l-128 -825q-10 -52 14 -82t95 -36v-57h-500z' /%3E %3Cglyph unicode='&%23xe050;' d='M-75 200h75v800h-75l125 167l125 -167h-75v-800h75l-125 -167zM300 900v300h150h700h150v-300h-50q0 29 -8 48.5t-18.5 30t-33.5 15t-39.5 5.5t-50.5 1h-200v-850l100 -50v-100h-400v100l100 50v850h-200q-34 0 -50.5 -1t-40 -5.5t-33.5 -15t-18.5 -30t-8.5 -48.5h-49z ' /%3E %3Cglyph unicode='&%23xe051;' d='M33 51l167 125v-75h800v75l167 -125l-167 -125v75h-800v-75zM100 901v300h150h700h150v-300h-50q0 29 -8 48.5t-18 30t-33.5 15t-40 5.5t-50.5 1h-200v-650l100 -50v-100h-400v100l100 50v650h-200q-34 0 -50.5 -1t-39.5 -5.5t-33.5 -15t-18.5 -30t-8 -48.5h-50z' /%3E %3Cglyph unicode='&%23xe052;' d='M0 50q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 350q0 -20 14.5 -35t35.5 -15h800q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-800q-21 0 -35.5 -14.5t-14.5 -35.5 v-100zM0 650q0 -20 14.5 -35t35.5 -15h1000q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1000q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 950q0 -20 14.5 -35t35.5 -15h600q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-600q-21 0 -35.5 -14.5 t-14.5 -35.5v-100z' /%3E %3Cglyph unicode='&%23xe053;' d='M0 50q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 650q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5 v-100zM200 350q0 -20 14.5 -35t35.5 -15h700q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-700q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM200 950q0 -20 14.5 -35t35.5 -15h700q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-700q-21 0 -35.5 -14.5 t-14.5 -35.5v-100z' /%3E %3Cglyph unicode='&%23xe054;' d='M0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM100 650v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1000q-21 0 -35.5 15 t-14.5 35zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM500 950v100q0 21 14.5 35.5t35.5 14.5h600q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-600 q-21 0 -35.5 15t-14.5 35z' /%3E %3Cglyph unicode='&%23xe055;' d='M0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM0 350v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15 t-14.5 35zM0 650v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM0 950v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100 q-21 0 -35.5 15t-14.5 35z' /%3E %3Cglyph unicode='&%23xe056;' d='M0 50v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM0 350v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15 t-14.5 35zM0 650v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM0 950v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15 t-14.5 35zM300 50v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800 q-21 0 -35.5 15t-14.5 35zM300 650v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM300 950v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15 h-800q-21 0 -35.5 15t-14.5 35z' /%3E %3Cglyph unicode='&%23xe057;' d='M-101 500v100h201v75l166 -125l-166 -125v75h-201zM300 0h100v1100h-100v-1100zM500 50q0 -20 14.5 -35t35.5 -15h600q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 350q0 -20 14.5 -35t35.5 -15h300q20 0 35 15t15 35 v100q0 21 -15 35.5t-35 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 650q0 -20 14.5 -35t35.5 -15h500q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 950q0 -20 14.5 -35t35.5 -15h100q20 0 35 15t15 35v100 q0 21 -15 35.5t-35 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-100z' /%3E %3Cglyph unicode='&%23xe058;' d='M1 50q0 -20 14.5 -35t35.5 -15h600q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 350q0 -20 14.5 -35t35.5 -15h300q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 650 q0 -20 14.5 -35t35.5 -15h500q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 950q0 -20 14.5 -35t35.5 -15h100q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM801 0v1100h100v-1100 h-100zM934 550l167 -125v75h200v100h-200v75z' /%3E %3Cglyph unicode='&%23xe059;' d='M0 275v650q0 31 22 53t53 22h750q31 0 53 -22t22 -53v-650q0 -31 -22 -53t-53 -22h-750q-31 0 -53 22t-22 53zM900 600l300 300v-600z' /%3E %3Cglyph unicode='&%23xe060;' d='M0 44v1012q0 18 13 31t31 13h1112q19 0 31.5 -13t12.5 -31v-1012q0 -18 -12.5 -31t-31.5 -13h-1112q-18 0 -31 13t-13 31zM100 263l247 182l298 -131l-74 156l293 318l236 -288v500h-1000v-737zM208 750q0 56 39 95t95 39t95 -39t39 -95t-39 -95t-95 -39t-95 39t-39 95z ' /%3E %3Cglyph unicode='&%23xe062;' d='M148 745q0 124 60.5 231.5t165 172t226.5 64.5q123 0 227 -63t164.5 -169.5t60.5 -229.5t-73 -272q-73 -114 -166.5 -237t-150.5 -189l-57 -66q-10 9 -27 26t-66.5 70.5t-96 109t-104 135.5t-100.5 155q-63 139 -63 262zM342 772q0 -107 75.5 -182.5t181.5 -75.5 q107 0 182.5 75.5t75.5 182.5t-75.5 182t-182.5 75t-182 -75.5t-75 -181.5z' /%3E %3Cglyph unicode='&%23xe063;' d='M1 600q0 122 47.5 233t127.5 191t191 127.5t233 47.5t233 -47.5t191 -127.5t127.5 -191t47.5 -233t-47.5 -233t-127.5 -191t-191 -127.5t-233 -47.5t-233 47.5t-191 127.5t-127.5 191t-47.5 233zM173 600q0 -177 125.5 -302t301.5 -125v854q-176 0 -301.5 -125 t-125.5 -302z' /%3E %3Cglyph unicode='&%23xe064;' d='M117 406q0 94 34 186t88.5 172.5t112 159t115 177t87.5 194.5q21 -71 57.5 -142.5t76 -130.5t83 -118.5t82 -117t70 -116t50 -125.5t18.5 -136q0 -89 -39 -165.5t-102 -126.5t-140 -79.5t-156 -33.5q-114 6 -211.5 53t-161.5 139t-64 210zM243 414q14 -82 59.5 -136 t136.5 -80l16 98q-7 6 -18 17t-34 48t-33 77q-15 73 -14 143.5t10 122.5l9 51q-92 -110 -119.5 -185t-12.5 -156z' /%3E %3Cglyph unicode='&%23xe065;' d='M0 400v300q0 165 117.5 282.5t282.5 117.5q366 -6 397 -14l-186 -186h-311q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v125l200 200v-225q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5 t-117.5 282.5zM436 341l161 50l412 412l-114 113l-405 -405zM995 1015l113 -113l113 113l-21 85l-92 28z' /%3E %3Cglyph unicode='&%23xe066;' d='M0 400v300q0 165 117.5 282.5t282.5 117.5h261l2 -80q-133 -32 -218 -120h-145q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5l200 153v-53q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5 zM423 524q30 38 81.5 64t103 35.5t99 14t77.5 3.5l29 -1v-209l360 324l-359 318v-216q-7 0 -19 -1t-48 -8t-69.5 -18.5t-76.5 -37t-76.5 -59t-62 -88t-39.5 -121.5z' /%3E %3Cglyph unicode='&%23xe067;' d='M0 400v300q0 165 117.5 282.5t282.5 117.5h300q61 0 127 -23l-178 -177h-349q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v69l200 200v-169q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5 t-117.5 282.5zM342 632l283 -284l567 567l-137 137l-430 -431l-146 147z' /%3E %3Cglyph unicode='&%23xe068;' d='M0 603l300 296v-198h200v200h-200l300 300l295 -300h-195v-200h200v198l300 -296l-300 -300v198h-200v-200h195l-295 -300l-300 300h200v200h-200v-198z' /%3E %3Cglyph unicode='&%23xe069;' d='M200 50v1000q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-437l500 487v-1100l-500 488v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5z' /%3E %3Cglyph unicode='&%23xe070;' d='M0 50v1000q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-437l500 487v-487l500 487v-1100l-500 488v-488l-500 488v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5z' /%3E %3Cglyph unicode='&%23xe071;' d='M136 550l564 550v-487l500 487v-1100l-500 488v-488z' /%3E %3Cglyph unicode='&%23xe072;' d='M200 0l900 550l-900 550v-1100z' /%3E %3Cglyph unicode='&%23xe073;' d='M200 150q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v800q0 21 -14.5 35.5t-35.5 14.5h-200q-21 0 -35.5 -14.5t-14.5 -35.5v-800zM600 150q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v800q0 21 -14.5 35.5t-35.5 14.5h-200 q-21 0 -35.5 -14.5t-14.5 -35.5v-800z' /%3E %3Cglyph unicode='&%23xe074;' d='M200 150q0 -20 14.5 -35t35.5 -15h800q21 0 35.5 15t14.5 35v800q0 21 -14.5 35.5t-35.5 14.5h-800q-21 0 -35.5 -14.5t-14.5 -35.5v-800z' /%3E %3Cglyph unicode='&%23xe075;' d='M0 0v1100l500 -487v487l564 -550l-564 -550v488z' /%3E %3Cglyph unicode='&%23xe076;' d='M0 0v1100l500 -487v487l500 -487v437q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-1000q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v438l-500 -488v488z' /%3E %3Cglyph unicode='&%23xe077;' d='M300 0v1100l500 -487v437q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-1000q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v438z' /%3E %3Cglyph unicode='&%23xe078;' d='M100 250v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1000q-21 0 -35.5 14.5t-14.5 35.5zM100 500h1100l-550 564z' /%3E %3Cglyph unicode='&%23xe079;' d='M185 599l592 -592l240 240l-353 353l353 353l-240 240z' /%3E %3Cglyph unicode='&%23xe080;' d='M272 194l353 353l-353 353l241 240l572 -571l21 -22l-1 -1v-1l-592 -591z' /%3E %3Cglyph unicode='&%23xe081;' d='M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM300 500h200v-200h200v200h200v200h-200v200h-200v-200h-200v-200z' /%3E %3Cglyph unicode='&%23xe082;' d='M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM300 500h600v200h-600v-200z' /%3E %3Cglyph unicode='&%23xe083;' d='M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM246 459l213 -213l141 142l141 -142l213 213l-142 141l142 141l-213 212l-141 -141l-141 142l-212 -213l141 -141 z' /%3E %3Cglyph unicode='&%23xe084;' d='M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM270 551l276 -277l411 411l-175 174l-236 -236l-102 102z' /%3E %3Cglyph unicode='&%23xe085;' d='M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM364 700h143q4 0 11.5 -1t11 -1t6.5 3t3 9t1 11t3.5 8.5t3.5 6t5.5 4t6.5 2.5t9 1.5t9 0.5h11.5h12.5 q19 0 30 -10t11 -26q0 -22 -4 -28t-27 -22q-5 -1 -12.5 -3t-27 -13.5t-34 -27t-26.5 -46t-11 -68.5h200q5 3 14 8t31.5 25.5t39.5 45.5t31 69t14 94q0 51 -17.5 89t-42 58t-58.5 32t-58.5 15t-51.5 3q-50 0 -90.5 -12t-75 -38.5t-53.5 -74.5t-19 -114zM500 300h200v100h-200 v-100z' /%3E %3Cglyph unicode='&%23xe086;' d='M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM400 300h400v100h-100v300h-300v-100h100v-200h-100v-100zM500 800h200v100h-200v-100z' /%3E %3Cglyph unicode='&%23xe087;' d='M0 500v200h195q31 125 98.5 199.5t206.5 100.5v200h200v-200q54 -20 113 -60t112.5 -105.5t71.5 -134.5h203v-200h-203q-25 -102 -116.5 -186t-180.5 -117v-197h-200v197q-140 27 -208 102.5t-98 200.5h-194zM290 500q24 -73 79.5 -127.5t130.5 -78.5v206h200v-206 q149 48 201 206h-201v200h200q-25 74 -75.5 127t-124.5 77v-204h-200v203q-75 -23 -130 -77t-79 -126h209v-200h-210z' /%3E %3Cglyph unicode='&%23xe088;' d='M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM356 465l135 135 l-135 135l109 109l135 -135l135 135l109 -109l-135 -135l135 -135l-109 -109l-135 135l-135 -135z' /%3E %3Cglyph unicode='&%23xe089;' d='M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM322 537l141 141 l87 -87l204 205l142 -142l-346 -345z' /%3E %3Cglyph unicode='&%23xe090;' d='M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -115 62 -215l568 567q-100 62 -216 62q-171 0 -292.5 -121.5t-121.5 -292.5zM391 245q97 -59 209 -59q171 0 292.5 121.5t121.5 292.5 q0 112 -59 209z' /%3E %3Cglyph unicode='&%23xe091;' d='M0 547l600 453v-300h600v-300h-600v-301z' /%3E %3Cglyph unicode='&%23xe092;' d='M0 400v300h600v300l600 -453l-600 -448v301h-600z' /%3E %3Cglyph unicode='&%23xe093;' d='M204 600l450 600l444 -600h-298v-600h-300v600h-296z' /%3E %3Cglyph unicode='&%23xe094;' d='M104 600h296v600h300v-600h298l-449 -600z' /%3E %3Cglyph unicode='&%23xe095;' d='M0 200q6 132 41 238.5t103.5 193t184 138t271.5 59.5v271l600 -453l-600 -448v301q-95 -2 -183 -20t-170 -52t-147 -92.5t-100 -135.5z' /%3E %3Cglyph unicode='&%23xe096;' d='M0 0v400l129 -129l294 294l142 -142l-294 -294l129 -129h-400zM635 777l142 -142l294 294l129 -129v400h-400l129 -129z' /%3E %3Cglyph unicode='&%23xe097;' d='M34 176l295 295l-129 129h400v-400l-129 130l-295 -295zM600 600v400l129 -129l295 295l142 -141l-295 -295l129 -130h-400z' /%3E %3Cglyph unicode='&%23xe101;' d='M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5t224.5 -45.5t184 -123t123 -184t45.5 -224.5t-45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5zM456 851l58 -302q4 -20 21.5 -34.5t37.5 -14.5h54q20 0 37.5 14.5 t21.5 34.5l58 302q4 20 -8 34.5t-32 14.5h-207q-21 0 -33 -14.5t-8 -34.5zM500 300h200v100h-200v-100z' /%3E %3Cglyph unicode='&%23xe102;' d='M0 800h100v-200h400v300h200v-300h400v200h100v100h-111q1 1 1 6.5t-1.5 15t-3.5 17.5l-34 172q-11 39 -41.5 63t-69.5 24q-32 0 -61 -17l-239 -144q-22 -13 -40 -35q-19 24 -40 36l-238 144q-33 18 -62 18q-39 0 -69.5 -23t-40.5 -61l-35 -177q-2 -8 -3 -18t-1 -15v-6 h-111v-100zM100 0h400v400h-400v-400zM200 900q-3 0 14 48t36 96l18 47l213 -191h-281zM700 0v400h400v-400h-400zM731 900l202 197q5 -12 12 -32.5t23 -64t25 -72t7 -28.5h-269z' /%3E %3Cglyph unicode='&%23xe103;' d='M0 -22v143l216 193q-9 53 -13 83t-5.5 94t9 113t38.5 114t74 124q47 60 99.5 102.5t103 68t127.5 48t145.5 37.5t184.5 43.5t220 58.5q0 -189 -22 -343t-59 -258t-89 -181.5t-108.5 -120t-122 -68t-125.5 -30t-121.5 -1.5t-107.5 12.5t-87.5 17t-56.5 7.5l-99 -55z M238.5 300.5q19.5 -6.5 86.5 76.5q55 66 367 234q70 38 118.5 69.5t102 79t99 111.5t86.5 148q22 50 24 60t-6 19q-7 5 -17 5t-26.5 -14.5t-33.5 -39.5q-35 -51 -113.5 -108.5t-139.5 -89.5l-61 -32q-369 -197 -458 -401q-48 -111 -28.5 -117.5z' /%3E %3Cglyph unicode='&%23xe104;' d='M111 408q0 -33 5 -63q9 -56 44 -119.5t105 -108.5q31 -21 64 -16t62 23.5t57 49.5t48 61.5t35 60.5q32 66 39 184.5t-13 157.5q79 -80 122 -164t26 -184q-5 -33 -20.5 -69.5t-37.5 -80.5q-10 -19 -14.5 -29t-12 -26t-9 -23.5t-3 -19t2.5 -15.5t11 -9.5t19.5 -5t30.5 2.5 t42 8q57 20 91 34t87.5 44.5t87 64t65.5 88.5t47 122q38 172 -44.5 341.5t-246.5 278.5q22 -44 43 -129q39 -159 -32 -154q-15 2 -33 9q-79 33 -120.5 100t-44 175.5t48.5 257.5q-13 -8 -34 -23.5t-72.5 -66.5t-88.5 -105.5t-60 -138t-8 -166.5q2 -12 8 -41.5t8 -43t6 -39.5 t3.5 -39.5t-1 -33.5t-6 -31.5t-13.5 -24t-21 -20.5t-31 -12q-38 -10 -67 13t-40.5 61.5t-15 81.5t10.5 75q-52 -46 -83.5 -101t-39 -107t-7.5 -85z' /%3E %3Cglyph unicode='&%23xe105;' d='M-61 600l26 40q6 10 20 30t49 63.5t74.5 85.5t97 90t116.5 83.5t132.5 59t145.5 23.5t145.5 -23.5t132.5 -59t116.5 -83.5t97 -90t74.5 -85.5t49 -63.5t20 -30l26 -40l-26 -40q-6 -10 -20 -30t-49 -63.5t-74.5 -85.5t-97 -90t-116.5 -83.5t-132.5 -59t-145.5 -23.5 t-145.5 23.5t-132.5 59t-116.5 83.5t-97 90t-74.5 85.5t-49 63.5t-20 30zM120 600q7 -10 40.5 -58t56 -78.5t68 -77.5t87.5 -75t103 -49.5t125 -21.5t123.5 20t100.5 45.5t85.5 71.5t66.5 75.5t58 81.5t47 66q-1 1 -28.5 37.5t-42 55t-43.5 53t-57.5 63.5t-58.5 54 q49 -74 49 -163q0 -124 -88 -212t-212 -88t-212 88t-88 212q0 85 46 158q-102 -87 -226 -258zM377 656q49 -124 154 -191l105 105q-37 24 -75 72t-57 84l-20 36z' /%3E %3Cglyph unicode='&%23xe106;' d='M-61 600l26 40q6 10 20 30t49 63.5t74.5 85.5t97 90t116.5 83.5t132.5 59t145.5 23.5q61 0 121 -17l37 142h148l-314 -1200h-148l37 143q-82 21 -165 71.5t-140 102t-109.5 112t-72 88.5t-29.5 43zM120 600q210 -282 393 -336l37 141q-107 18 -178.5 101.5t-71.5 193.5 q0 85 46 158q-102 -87 -226 -258zM377 656q49 -124 154 -191l47 47l23 87q-30 28 -59 69t-44 68l-14 26zM780 161l38 145q22 15 44.5 34t46 44t40.5 44t41 50.5t33.5 43.5t33 44t24.5 34q-97 127 -140 175l39 146q67 -54 131.5 -125.5t87.5 -103.5t36 -52l26 -40l-26 -40 q-7 -12 -25.5 -38t-63.5 -79.5t-95.5 -102.5t-124 -100t-146.5 -79z' /%3E %3Cglyph unicode='&%23xe107;' d='M-97.5 34q13.5 -34 50.5 -34h1294q37 0 50.5 35.5t-7.5 67.5l-642 1056q-20 34 -48 36.5t-48 -29.5l-642 -1066q-21 -32 -7.5 -66zM155 200l445 723l445 -723h-345v100h-200v-100h-345zM500 600l100 -300l100 300v100h-200v-100z' /%3E %3Cglyph unicode='&%23xe108;' d='M100 262v41q0 20 11 44.5t26 38.5l363 325v339q0 62 44 106t106 44t106 -44t44 -106v-339l363 -325q15 -14 26 -38.5t11 -44.5v-41q0 -20 -12 -26.5t-29 5.5l-359 249v-263q100 -91 100 -113v-64q0 -20 -13 -28.5t-32 0.5l-94 78h-222l-94 -78q-19 -9 -32 -0.5t-13 28.5 v64q0 22 100 113v263l-359 -249q-17 -12 -29 -5.5t-12 26.5z' /%3E %3Cglyph unicode='&%23xe109;' d='M0 50q0 -20 14.5 -35t35.5 -15h1000q21 0 35.5 15t14.5 35v750h-1100v-750zM0 900h1100v150q0 21 -14.5 35.5t-35.5 14.5h-150v100h-100v-100h-500v100h-100v-100h-150q-21 0 -35.5 -14.5t-14.5 -35.5v-150zM100 100v100h100v-100h-100zM100 300v100h100v-100h-100z M100 500v100h100v-100h-100zM300 100v100h100v-100h-100zM300 300v100h100v-100h-100zM300 500v100h100v-100h-100zM500 100v100h100v-100h-100zM500 300v100h100v-100h-100zM500 500v100h100v-100h-100zM700 100v100h100v-100h-100zM700 300v100h100v-100h-100zM700 500 v100h100v-100h-100zM900 100v100h100v-100h-100zM900 300v100h100v-100h-100zM900 500v100h100v-100h-100z' /%3E %3Cglyph unicode='&%23xe110;' d='M0 200v200h259l600 600h241v198l300 -295l-300 -300v197h-159l-600 -600h-341zM0 800h259l122 -122l141 142l-181 180h-341v-200zM678 381l141 142l122 -123h159v198l300 -295l-300 -300v197h-241z' /%3E %3Cglyph unicode='&%23xe111;' d='M0 400v600q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-596l-304 -300v300h-100q-41 0 -70.5 29.5t-29.5 70.5z' /%3E %3Cglyph unicode='&%23xe112;' d='M100 600v200h300v-250q0 -113 6 -145q17 -92 102 -117q39 -11 92 -11q37 0 66.5 5.5t50 15.5t36 24t24 31.5t14 37.5t7 42t2.5 45t0 47v25v250h300v-200q0 -42 -3 -83t-15 -104t-31.5 -116t-58 -109.5t-89 -96.5t-129 -65.5t-174.5 -25.5t-174.5 25.5t-129 65.5t-89 96.5 t-58 109.5t-31.5 116t-15 104t-3 83zM100 900v300h300v-300h-300zM800 900v300h300v-300h-300z' /%3E %3Cglyph unicode='&%23xe113;' d='M-30 411l227 -227l352 353l353 -353l226 227l-578 579z' /%3E %3Cglyph unicode='&%23xe114;' d='M70 797l580 -579l578 579l-226 227l-353 -353l-352 353z' /%3E %3Cglyph unicode='&%23xe115;' d='M-198 700l299 283l300 -283h-203v-400h385l215 -200h-800v600h-196zM402 1000l215 -200h381v-400h-198l299 -283l299 283h-200v600h-796z' /%3E %3Cglyph unicode='&%23xe116;' d='M18 939q-5 24 10 42q14 19 39 19h896l38 162q5 17 18.5 27.5t30.5 10.5h94q20 0 35 -14.5t15 -35.5t-15 -35.5t-35 -14.5h-54l-201 -961q-2 -4 -6 -10.5t-19 -17.5t-33 -11h-31v-50q0 -20 -14.5 -35t-35.5 -15t-35.5 15t-14.5 35v50h-300v-50q0 -20 -14.5 -35t-35.5 -15 t-35.5 15t-14.5 35v50h-50q-21 0 -35.5 15t-14.5 35q0 21 14.5 35.5t35.5 14.5h535l48 200h-633q-32 0 -54.5 21t-27.5 43z' /%3E %3Cglyph unicode='&%23xe117;' d='M0 0v800h1200v-800h-1200zM0 900v100h200q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5h500v-100h-1200z' /%3E %3Cglyph unicode='&%23xe118;' d='M1 0l300 700h1200l-300 -700h-1200zM1 400v600h200q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5h500v-200h-1000z' /%3E %3Cglyph unicode='&%23xe119;' d='M302 300h198v600h-198l298 300l298 -300h-198v-600h198l-298 -300z' /%3E %3Cglyph unicode='&%23xe120;' d='M0 600l300 298v-198h600v198l300 -298l-300 -297v197h-600v-197z' /%3E %3Cglyph unicode='&%23xe121;' d='M0 100v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM31 400l172 739q5 22 23 41.5t38 19.5h672q19 0 37.5 -22.5t23.5 -45.5l172 -732h-1138zM800 100h100v100h-100v-100z M1000 100h100v100h-100v-100z' /%3E %3Cglyph unicode='&%23xe122;' d='M-101 600v50q0 24 25 49t50 38l25 13v-250l-11 5.5t-24 14t-30 21.5t-24 27.5t-11 31.5zM100 500v250v8v8v7t0.5 7t1.5 5.5t2 5t3 4t4.5 3.5t6 1.5t7.5 0.5h200l675 250v-850l-675 200h-38l47 -276q2 -12 -3 -17.5t-11 -6t-21 -0.5h-8h-83q-20 0 -34.5 14t-18.5 35 q-55 337 -55 351zM1100 200v850q0 21 14.5 35.5t35.5 14.5q20 0 35 -14.5t15 -35.5v-850q0 -20 -15 -35t-35 -15q-21 0 -35.5 15t-14.5 35z' /%3E %3Cglyph unicode='&%23xe123;' d='M74 350q0 21 13.5 35.5t33.5 14.5h18l117 173l63 327q15 77 76 140t144 83l-18 32q-6 19 3 32t29 13h94q20 0 29 -10.5t3 -29.5q-18 -36 -18 -37q83 -19 144 -82.5t76 -140.5l63 -327l118 -173h17q20 0 33.5 -14.5t13.5 -35.5q0 -20 -13 -40t-31 -27q-8 -3 -23 -8.5 t-65 -20t-103 -25t-132.5 -19.5t-158.5 -9q-125 0 -245.5 20.5t-178.5 40.5l-58 20q-18 7 -31 27.5t-13 40.5zM497 110q12 -49 40 -79.5t63 -30.5t63 30.5t39 79.5q-48 -6 -102 -6t-103 6z' /%3E %3Cglyph unicode='&%23xe124;' d='M21 445l233 -45l-78 -224l224 78l45 -233l155 179l155 -179l45 233l224 -78l-78 224l234 45l-180 155l180 156l-234 44l78 225l-224 -78l-45 233l-155 -180l-155 180l-45 -233l-224 78l78 -225l-233 -44l179 -156z' /%3E %3Cglyph unicode='&%23xe125;' d='M0 200h200v600h-200v-600zM300 275q0 -75 100 -75h61q124 -100 139 -100h250q46 0 83 57l238 344q29 31 29 74v100q0 44 -30.5 84.5t-69.5 40.5h-328q28 118 28 125v150q0 44 -30.5 84.5t-69.5 40.5h-50q-27 0 -51 -20t-38 -48l-96 -198l-145 -196q-20 -26 -20 -63v-400z M400 300v375l150 213l100 212h50v-175l-50 -225h450v-125l-250 -375h-214l-136 100h-100z' /%3E %3Cglyph unicode='&%23xe126;' d='M0 400v600h200v-600h-200zM300 525v400q0 75 100 75h61q124 100 139 100h250q46 0 83 -57l238 -344q29 -31 29 -74v-100q0 -44 -30.5 -84.5t-69.5 -40.5h-328q28 -118 28 -125v-150q0 -44 -30.5 -84.5t-69.5 -40.5h-50q-27 0 -51 20t-38 48l-96 198l-145 196 q-20 26 -20 63zM400 525l150 -212l100 -213h50v175l-50 225h450v125l-250 375h-214l-136 -100h-100v-375z' /%3E %3Cglyph unicode='&%23xe127;' d='M8 200v600h200v-600h-200zM308 275v525q0 17 14 35.5t28 28.5l14 9l362 230q14 6 25 6q17 0 29 -12l109 -112q14 -14 14 -34q0 -18 -11 -32l-85 -121h302q85 0 138.5 -38t53.5 -110t-54.5 -111t-138.5 -39h-107l-130 -339q-7 -22 -20.5 -41.5t-28.5 -19.5h-341 q-7 0 -90 81t-83 94zM408 289l100 -89h293l131 339q6 21 19.5 41t28.5 20h203q16 0 25 15t9 36q0 20 -9 34.5t-25 14.5h-457h-6.5h-7.5t-6.5 0.5t-6 1t-5 1.5t-5.5 2.5t-4 4t-4 5.5q-5 12 -5 20q0 14 10 27l147 183l-86 83l-339 -236v-503z' /%3E %3Cglyph unicode='&%23xe128;' d='M-101 651q0 72 54 110t139 38l302 -1l-85 121q-11 16 -11 32q0 21 14 34l109 113q13 12 29 12q11 0 25 -6l365 -230q7 -4 17 -10.5t26.5 -26t16.5 -36.5v-526q0 -13 -86 -93.5t-94 -80.5h-341q-16 0 -29.5 20t-19.5 41l-130 339h-107q-84 0 -139 39t-55 111zM-1 601h222 q15 0 28.5 -20.5t19.5 -40.5l131 -339h293l107 89v502l-343 237l-87 -83l145 -184q10 -11 10 -26q0 -11 -5 -20q-1 -3 -3.5 -5.5l-4 -4t-5 -2.5t-5.5 -1.5t-6.5 -1t-6.5 -0.5h-7.5h-6.5h-476v-100zM1000 201v600h200v-600h-200z' /%3E %3Cglyph unicode='&%23xe129;' d='M97 719l230 -363q4 -6 10.5 -15.5t26 -25t36.5 -15.5h525q13 0 94 83t81 90v342q0 15 -20 28.5t-41 19.5l-339 131v106q0 84 -39 139t-111 55t-110 -53.5t-38 -138.5v-302l-121 84q-15 12 -33.5 11.5t-32.5 -13.5l-112 -110q-22 -22 -6 -53zM172 739l83 86l183 -146 q22 -18 47 -5q3 1 5.5 3.5l4 4t2.5 5t1.5 5.5t1 6.5t0.5 6.5v7.5v6.5v456q0 22 25 31t50 -0.5t25 -30.5v-202q0 -16 20 -29.5t41 -19.5l339 -130v-294l-89 -100h-503zM400 0v200h600v-200h-600z' /%3E %3Cglyph unicode='&%23xe130;' d='M2 585q-16 -31 6 -53l112 -110q13 -13 32 -13.5t34 10.5l121 85q0 -51 -0.5 -153.5t-0.5 -148.5q0 -84 38.5 -138t110.5 -54t111 55t39 139v106l339 131q20 6 40.5 19.5t20.5 28.5v342q0 7 -81 90t-94 83h-525q-17 0 -35.5 -14t-28.5 -28l-10 -15zM77 565l236 339h503 l89 -100v-294l-340 -130q-20 -6 -40 -20t-20 -29v-202q0 -22 -25 -31t-50 0t-25 31v456v14.5t-1.5 11.5t-5 12t-9.5 7q-24 13 -46 -5l-184 -146zM305 1104v200h600v-200h-600z' /%3E %3Cglyph unicode='&%23xe131;' d='M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q162 0 299.5 -80t217.5 -218t80 -300t-80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM298 701l2 -201h300l-2 -194l402 294l-402 298v-197h-300z' /%3E %3Cglyph unicode='&%23xe132;' d='M0 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t231.5 47.5q122 0 232.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-218 -217.5t-300 -80t-299.5 80t-217.5 217.5t-80 299.5zM200 600l402 -294l-2 194h300l2 201h-300v197z' /%3E %3Cglyph unicode='&%23xe133;' d='M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q162 0 299.5 -80t217.5 -218t80 -300t-80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM300 600h200v-300h200v300h200l-300 400z' /%3E %3Cglyph unicode='&%23xe134;' d='M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q162 0 299.5 -80t217.5 -218t80 -300t-80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM300 600l300 -400l300 400h-200v300h-200v-300h-200z' /%3E %3Cglyph unicode='&%23xe135;' d='M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q121 0 231.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM254 780q-8 -33 5.5 -92.5t7.5 -87.5q0 -9 17 -44t16 -60 q12 0 23 -5.5t23 -15t20 -13.5q24 -12 108 -42q22 -8 53 -31.5t59.5 -38.5t57.5 -11q8 -18 -15 -55t-20 -57q42 -71 87 -80q0 -6 -3 -15.5t-3.5 -14.5t4.5 -17q104 -3 221 112q30 29 47 47t34.5 49t20.5 62q-14 9 -37 9.5t-36 7.5q-14 7 -49 15t-52 19q-9 0 -39.5 -0.5 t-46.5 -1.5t-39 -6.5t-39 -16.5q-50 -35 -66 -12q-4 2 -3.5 25.5t0.5 25.5q-6 13 -26.5 17t-24.5 7q2 22 -2 41t-16.5 28t-38.5 -20q-23 -25 -42 4q-19 28 -8 58q6 16 22 22q6 -1 26 -1.5t33.5 -4t19.5 -13.5q12 -19 32 -37.5t34 -27.5l14 -8q0 3 9.5 39.5t5.5 57.5 q-4 23 14.5 44.5t22.5 31.5q5 14 10 35t8.5 31t15.5 22.5t34 21.5q-6 18 10 37q8 0 23.5 -1.5t24.5 -1.5t20.5 4.5t20.5 15.5q-10 23 -30.5 42.5t-38 30t-49 26.5t-43.5 23q11 39 2 44q31 -13 58 -14.5t39 3.5l11 4q7 36 -16.5 53.5t-64.5 28.5t-56 23q-19 -3 -37 0 q-15 -12 -36.5 -21t-34.5 -12t-44 -8t-39 -6q-15 -3 -45.5 0.5t-45.5 -2.5q-21 -7 -52 -26.5t-34 -34.5q-3 -11 6.5 -22.5t8.5 -18.5q-3 -34 -27.5 -90.5t-29.5 -79.5zM518 916q3 12 16 30t16 25q10 -10 18.5 -10t14 6t14.5 14.5t16 12.5q0 -24 17 -66.5t17 -43.5 q-9 2 -31 5t-36 5t-32 8t-30 14zM692 1003h1h-1z' /%3E %3Cglyph unicode='&%23xe136;' d='M0 164.5q0 21.5 15 37.5l600 599q-33 101 6 201.5t135 154.5q164 92 306 -9l-259 -138l145 -232l251 126q13 -175 -151 -267q-123 -70 -253 -23l-596 -596q-15 -16 -36.5 -16t-36.5 16l-111 110q-15 15 -15 36.5z' /%3E %3Cglyph unicode='&%23xe137;' horiz-adv-x='1220' d='M0 196v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM0 596v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000 q-41 0 -70.5 29.5t-29.5 70.5zM0 996v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM600 596h500v100h-500v-100zM800 196h300v100h-300v-100zM900 996h200v100h-200v-100z' /%3E %3Cglyph unicode='&%23xe138;' d='M100 1100v100h1000v-100h-1000zM150 1000h900l-350 -500v-300l-200 -200v500z' /%3E %3Cglyph unicode='&%23xe139;' d='M0 200v200h1200v-200q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM0 500v400q0 41 29.5 70.5t70.5 29.5h300v100q0 41 29.5 70.5t70.5 29.5h200q41 0 70.5 -29.5t29.5 -70.5v-100h300q41 0 70.5 -29.5t29.5 -70.5v-400h-500v100h-200v-100h-500z M500 1000h200v100h-200v-100z' /%3E %3Cglyph unicode='&%23xe140;' d='M0 0v400l129 -129l200 200l142 -142l-200 -200l129 -129h-400zM0 800l129 129l200 -200l142 142l-200 200l129 129h-400v-400zM729 329l142 142l200 -200l129 129v-400h-400l129 129zM729 871l200 200l-129 129h400v-400l-129 129l-200 -200z' /%3E %3Cglyph unicode='&%23xe141;' d='M0 596q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM182 596q0 -172 121.5 -293t292.5 -121t292.5 121t121.5 293q0 171 -121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM291 655 q0 23 15.5 38.5t38.5 15.5t39 -16t16 -38q0 -23 -16 -39t-39 -16q-22 0 -38 16t-16 39zM400 850q0 22 16 38.5t39 16.5q22 0 38 -16t16 -39t-16 -39t-38 -16q-23 0 -39 16.5t-16 38.5zM514 609q0 32 20.5 56.5t51.5 29.5l122 126l1 1q-9 14 -9 28q0 22 16 38.5t39 16.5 q22 0 38 -16t16 -39t-16 -39t-38 -16q-14 0 -29 10l-55 -145q17 -22 17 -51q0 -36 -25.5 -61.5t-61.5 -25.5t-61.5 25.5t-25.5 61.5zM800 655q0 22 16 38t39 16t38.5 -15.5t15.5 -38.5t-16 -39t-38 -16q-23 0 -39 16t-16 39z' /%3E %3Cglyph unicode='&%23xe142;' d='M-40 375q-13 -95 35 -173q35 -57 94 -89t129 -32q63 0 119 28q33 16 65 40.5t52.5 45.5t59.5 64q40 44 57 61l394 394q35 35 47 84t-3 96q-27 87 -117 104q-20 2 -29 2q-46 0 -78.5 -16.5t-67.5 -51.5l-389 -396l-7 -7l69 -67l377 373q20 22 39 38q23 23 50 23 q38 0 53 -36q16 -39 -20 -75l-547 -547q-52 -52 -125 -52q-55 0 -100 33t-54 96q-5 35 2.5 66t31.5 63t42 50t56 54q24 21 44 41l348 348q52 52 82.5 79.5t84 54t107.5 26.5q25 0 48 -4q95 -17 154 -94.5t51 -175.5q-7 -101 -98 -192l-252 -249l-253 -256l7 -7l69 -60 l517 511q67 67 95 157t11 183q-16 87 -67 154t-130 103q-69 33 -152 33q-107 0 -197 -55q-40 -24 -111 -95l-512 -512q-68 -68 -81 -163z' /%3E %3Cglyph unicode='&%23xe143;' d='M80 784q0 131 98.5 229.5t230.5 98.5q143 0 241 -129q103 129 246 129q129 0 226 -98.5t97 -229.5q0 -46 -17.5 -91t-61 -99t-77 -89.5t-104.5 -105.5q-197 -191 -293 -322l-17 -23l-16 23q-43 58 -100 122.5t-92 99.5t-101 100q-71 70 -104.5 105.5t-77 89.5t-61 99 t-17.5 91zM250 784q0 -27 30.5 -70t61.5 -75.5t95 -94.5l22 -22q93 -90 190 -201q82 92 195 203l12 12q64 62 97.5 97t64.5 79t31 72q0 71 -48 119.5t-105 48.5q-74 0 -132 -83l-118 -171l-114 174q-51 80 -123 80q-60 0 -109.5 -49.5t-49.5 -118.5z' /%3E %3Cglyph unicode='&%23xe144;' d='M57 353q0 -95 66 -159l141 -142q68 -66 159 -66q93 0 159 66l283 283q66 66 66 159t-66 159l-141 141q-8 9 -19 17l-105 -105l212 -212l-389 -389l-247 248l95 95l-18 18q-46 45 -75 101l-55 -55q-66 -66 -66 -159zM269 706q0 -93 66 -159l141 -141q7 -7 19 -17l105 105 l-212 212l389 389l247 -247l-95 -96l18 -17q47 -49 77 -100l29 29q35 35 62.5 88t27.5 96q0 93 -66 159l-141 141q-66 66 -159 66q-95 0 -159 -66l-283 -283q-66 -64 -66 -159z' /%3E %3Cglyph unicode='&%23xe145;' d='M200 100v953q0 21 30 46t81 48t129 38t163 15t162 -15t127 -38t79 -48t29 -46v-953q0 -41 -29.5 -70.5t-70.5 -29.5h-600q-41 0 -70.5 29.5t-29.5 70.5zM300 300h600v700h-600v-700zM496 150q0 -43 30.5 -73.5t73.5 -30.5t73.5 30.5t30.5 73.5t-30.5 73.5t-73.5 30.5 t-73.5 -30.5t-30.5 -73.5z' /%3E %3Cglyph unicode='&%23xe146;' d='M0 0l303 380l207 208l-210 212h300l267 279l-35 36q-15 14 -15 35t15 35q14 15 35 15t35 -15l283 -282q15 -15 15 -36t-15 -35q-14 -15 -35 -15t-35 15l-36 35l-279 -267v-300l-212 210l-208 -207z' /%3E %3Cglyph unicode='&%23xe148;' d='M295 433h139q5 -77 48.5 -126.5t117.5 -64.5v335q-6 1 -15.5 4t-11.5 3q-46 14 -79 26.5t-72 36t-62.5 52t-40 72.5t-16.5 99q0 92 44 159.5t109 101t144 40.5v78h100v-79q38 -4 72.5 -13.5t75.5 -31.5t71 -53.5t51.5 -84t24.5 -118.5h-159q-8 72 -35 109.5t-101 50.5 v-307l64 -14q34 -7 64 -16.5t70 -31.5t67.5 -52t47.5 -80.5t20 -112.5q0 -139 -89 -224t-244 -96v-77h-100v78q-152 17 -237 104q-40 40 -52.5 93.5t-15.5 139.5zM466 889q0 -29 8 -51t16.5 -34t29.5 -22.5t31 -13.5t38 -10q7 -2 11 -3v274q-61 -8 -97.5 -37.5t-36.5 -102.5 zM700 237q170 18 170 151q0 64 -44 99.5t-126 60.5v-311z' /%3E %3Cglyph unicode='&%23xe149;' d='M100 600v100h166q-24 49 -44 104q-10 26 -14.5 55.5t-3 72.5t25 90t68.5 87q97 88 263 88q129 0 230 -89t101 -208h-153q0 52 -34 89.5t-74 51.5t-76 14q-37 0 -79 -14.5t-62 -35.5q-41 -44 -41 -101q0 -28 16.5 -69.5t28 -62.5t41.5 -72h241v-100h-197q8 -50 -2.5 -115 t-31.5 -94q-41 -59 -99 -113q35 11 84 18t70 7q33 1 103 -16t103 -17q76 0 136 30l50 -147q-41 -25 -80.5 -36.5t-59 -13t-61.5 -1.5q-23 0 -128 33t-155 29q-39 -4 -82 -17t-66 -25l-24 -11l-55 145l16.5 11t15.5 10t13.5 9.5t14.5 12t14.5 14t17.5 18.5q48 55 54 126.5 t-30 142.5h-221z' /%3E %3Cglyph unicode='&%23xe150;' d='M2 300l298 -300l298 300h-198v900h-200v-900h-198zM602 900l298 300l298 -300h-198v-900h-200v900h-198z' /%3E %3Cglyph unicode='&%23xe151;' d='M2 300h198v900h200v-900h198l-298 -300zM700 0v200h100v-100h200v-100h-300zM700 400v100h300v-200h-99v-100h-100v100h99v100h-200zM700 700v500h300v-500h-100v100h-100v-100h-100zM801 900h100v200h-100v-200z' /%3E %3Cglyph unicode='&%23xe152;' d='M2 300h198v900h200v-900h198l-298 -300zM700 0v500h300v-500h-100v100h-100v-100h-100zM700 700v200h100v-100h200v-100h-300zM700 1100v100h300v-200h-99v-100h-100v100h99v100h-200zM801 200h100v200h-100v-200z' /%3E %3Cglyph unicode='&%23xe153;' d='M2 300l298 -300l298 300h-198v900h-200v-900h-198zM800 100v400h300v-500h-100v100h-200zM800 1100v100h200v-500h-100v400h-100zM901 200h100v200h-100v-200z' /%3E %3Cglyph unicode='&%23xe154;' d='M2 300l298 -300l298 300h-198v900h-200v-900h-198zM800 400v100h200v-500h-100v400h-100zM800 800v400h300v-500h-100v100h-200zM901 900h100v200h-100v-200z' /%3E %3Cglyph unicode='&%23xe155;' d='M2 300l298 -300l298 300h-198v900h-200v-900h-198zM700 100v200h500v-200h-500zM700 400v200h400v-200h-400zM700 700v200h300v-200h-300zM700 1000v200h200v-200h-200z' /%3E %3Cglyph unicode='&%23xe156;' d='M2 300l298 -300l298 300h-198v900h-200v-900h-198zM700 100v200h200v-200h-200zM700 400v200h300v-200h-300zM700 700v200h400v-200h-400zM700 1000v200h500v-200h-500z' /%3E %3Cglyph unicode='&%23xe157;' d='M0 400v300q0 165 117.5 282.5t282.5 117.5h300q162 0 281 -118.5t119 -281.5v-300q0 -165 -118.5 -282.5t-281.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500z' /%3E %3Cglyph unicode='&%23xe158;' d='M0 400v300q0 163 119 281.5t281 118.5h300q165 0 282.5 -117.5t117.5 -282.5v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-163 0 -281.5 117.5t-118.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM400 300l333 250l-333 250v-500z' /%3E %3Cglyph unicode='&%23xe159;' d='M0 400v300q0 163 117.5 281.5t282.5 118.5h300q163 0 281.5 -119t118.5 -281v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM300 700l250 -333l250 333h-500z' /%3E %3Cglyph unicode='&%23xe160;' d='M0 400v300q0 165 117.5 282.5t282.5 117.5h300q165 0 282.5 -117.5t117.5 -282.5v-300q0 -162 -118.5 -281t-281.5 -119h-300q-165 0 -282.5 118.5t-117.5 281.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM300 400h500l-250 333z' /%3E %3Cglyph unicode='&%23xe161;' d='M0 400v300h300v200l400 -350l-400 -350v200h-300zM500 0v200h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5h-500v200h400q165 0 282.5 -117.5t117.5 -282.5v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-400z' /%3E %3Cglyph unicode='&%23xe162;' d='M217 519q8 -19 31 -19h302q-155 -438 -160 -458q-5 -21 4 -32l9 -8h9q14 0 26 15q11 13 274.5 321.5t264.5 308.5q14 19 5 36q-8 17 -31 17l-301 -1q1 4 78 219.5t79 227.5q2 15 -5 27l-9 9h-9q-15 0 -25 -16q-4 -6 -98 -111.5t-228.5 -257t-209.5 -237.5q-16 -19 -6 -41 z' /%3E %3Cglyph unicode='&%23xe163;' d='M0 400q0 -165 117.5 -282.5t282.5 -117.5h300q47 0 100 15v185h-500q-41 0 -70.5 29.5t-29.5 70.5v500q0 41 29.5 70.5t70.5 29.5h500v185q-14 4 -114 7.5t-193 5.5l-93 2q-165 0 -282.5 -117.5t-117.5 -282.5v-300zM600 400v300h300v200l400 -350l-400 -350v200h-300z ' /%3E %3Cglyph unicode='&%23xe164;' d='M0 400q0 -165 117.5 -282.5t282.5 -117.5h300q163 0 281.5 117.5t118.5 282.5v98l-78 73l-122 -123v-148q0 -41 -29.5 -70.5t-70.5 -29.5h-500q-41 0 -70.5 29.5t-29.5 70.5v500q0 41 29.5 70.5t70.5 29.5h156l118 122l-74 78h-100q-165 0 -282.5 -117.5t-117.5 -282.5 v-300zM496 709l353 342l-149 149h500v-500l-149 149l-342 -353z' /%3E %3Cglyph unicode='&%23xe165;' d='M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM406 600 q0 80 57 137t137 57t137 -57t57 -137t-57 -137t-137 -57t-137 57t-57 137z' /%3E %3Cglyph unicode='&%23xe166;' d='M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 800l445 -500l450 500h-295v400h-300v-400h-300zM900 150h100v50h-100v-50z' /%3E %3Cglyph unicode='&%23xe167;' d='M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 700h300v-300h300v300h295l-445 500zM900 150h100v50h-100v-50z' /%3E %3Cglyph unicode='&%23xe168;' d='M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 705l305 -305l596 596l-154 155l-442 -442l-150 151zM900 150h100v50h-100v-50z' /%3E %3Cglyph unicode='&%23xe169;' d='M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 988l97 -98l212 213l-97 97zM200 400l697 1l3 699l-250 -239l-149 149l-212 -212l149 -149zM900 150h100v50h-100v-50z' /%3E %3Cglyph unicode='&%23xe170;' d='M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM200 612l212 -212l98 97l-213 212zM300 1200l239 -250l-149 -149l212 -212l149 148l249 -237l-1 697zM900 150h100v50h-100v-50z' /%3E %3Cglyph unicode='&%23xe171;' d='M23 415l1177 784v-1079l-475 272l-310 -393v416h-392zM494 210l672 938l-672 -712v-226z' /%3E %3Cglyph unicode='&%23xe172;' d='M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-850q0 -21 -15 -35.5t-35 -14.5h-150v400h-700v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 1000h100v200h-100v-200z' /%3E %3Cglyph unicode='&%23xe173;' d='M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-218l-276 -275l-120 120l-126 -127h-378v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM581 306l123 123l120 -120l353 352l123 -123l-475 -476zM600 1000h100v200h-100v-200z' /%3E %3Cglyph unicode='&%23xe174;' d='M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-269l-103 -103l-170 170l-298 -298h-329v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 1000h100v200h-100v-200zM700 133l170 170l-170 170l127 127l170 -170l170 170l127 -128l-170 -169l170 -170 l-127 -127l-170 170l-170 -170z' /%3E %3Cglyph unicode='&%23xe175;' d='M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-300h-400v-200h-500v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 300l300 -300l300 300h-200v300h-200v-300h-200zM600 1000v200h100v-200h-100z' /%3E %3Cglyph unicode='&%23xe176;' d='M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-402l-200 200l-298 -298h-402v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 300h200v-300h200v300h200l-300 300zM600 1000v200h100v-200h-100z' /%3E %3Cglyph unicode='&%23xe177;' d='M0 250q0 -21 14.5 -35.5t35.5 -14.5h1100q21 0 35.5 14.5t14.5 35.5v550h-1200v-550zM0 900h1200v150q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-150zM100 300v200h400v-200h-400z' /%3E %3Cglyph unicode='&%23xe178;' d='M0 400l300 298v-198h400v-200h-400v-198zM100 800v200h100v-200h-100zM300 800v200h100v-200h-100zM500 800v200h400v198l300 -298l-300 -298v198h-400zM800 300v200h100v-200h-100zM1000 300h100v200h-100v-200z' /%3E %3Cglyph unicode='&%23xe179;' d='M100 700v400l50 100l50 -100v-300h100v300l50 100l50 -100v-300h100v300l50 100l50 -100v-400l-100 -203v-447q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v447zM800 597q0 -29 10.5 -55.5t25 -43t29 -28.5t25.5 -18l10 -5v-397q0 -21 14.5 -35.5 t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v1106q0 31 -18 40.5t-44 -7.5l-276 -116q-25 -17 -43.5 -51.5t-18.5 -65.5v-359z' /%3E %3Cglyph unicode='&%23xe180;' d='M100 0h400v56q-75 0 -87.5 6t-12.5 44v394h500v-394q0 -38 -12.5 -44t-87.5 -6v-56h400v56q-4 0 -11 0.5t-24 3t-30 7t-24 15t-11 24.5v888q0 22 25 34.5t50 13.5l25 2v56h-400v-56q75 0 87.5 -6t12.5 -44v-394h-500v394q0 38 12.5 44t87.5 6v56h-400v-56q4 0 11 -0.5 t24 -3t30 -7t24 -15t11 -24.5v-888q0 -22 -25 -34.5t-50 -13.5l-25 -2v-56z' /%3E %3Cglyph unicode='&%23xe181;' d='M0 300q0 -41 29.5 -70.5t70.5 -29.5h300q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5h-300q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM100 100h400l200 200h105l295 98v-298h-425l-100 -100h-375zM100 300v200h300v-200h-300zM100 600v200h300v-200h-300z M100 1000h400l200 -200v-98l295 98h105v200h-425l-100 100h-375zM700 402v163l400 133v-163z' /%3E %3Cglyph unicode='&%23xe182;' d='M16.5 974.5q0.5 -21.5 16 -90t46.5 -140t104 -177.5t175 -208q103 -103 207.5 -176t180 -103.5t137 -47t92.5 -16.5l31 1l163 162q17 18 13.5 41t-22.5 37l-192 136q-19 14 -45 12t-42 -19l-118 -118q-142 101 -268 227t-227 268l118 118q17 17 20 41.5t-11 44.5 l-139 194q-14 19 -36.5 22t-40.5 -14l-162 -162q-1 -11 -0.5 -32.5z' /%3E %3Cglyph unicode='&%23xe183;' d='M0 50v212q0 20 10.5 45.5t24.5 39.5l365 303v50q0 4 1 10.5t12 22.5t30 28.5t60 23t97 10.5t97 -10t60 -23.5t30 -27.5t12 -24l1 -10v-50l365 -303q14 -14 24.5 -39.5t10.5 -45.5v-212q0 -21 -14.5 -35.5t-35.5 -14.5h-1100q-20 0 -35 14.5t-15 35.5zM0 712 q0 -21 14.5 -33.5t34.5 -8.5l202 33q20 4 34.5 21t14.5 38v146q141 24 300 24t300 -24v-146q0 -21 14.5 -38t34.5 -21l202 -33q20 -4 34.5 8.5t14.5 33.5v200q-6 8 -19 20.5t-63 45t-112 57t-171 45t-235 20.5q-92 0 -175 -10.5t-141.5 -27t-108.5 -36.5t-81.5 -40 t-53.5 -36.5t-31 -27.5l-9 -10v-200z' /%3E %3Cglyph unicode='&%23xe184;' d='M100 0v100h1100v-100h-1100zM175 200h950l-125 150v250l100 100v400h-100v-200h-100v200h-200v-200h-100v200h-200v-200h-100v200h-100v-400l100 -100v-250z' /%3E %3Cglyph unicode='&%23xe185;' d='M100 0h300v400q0 41 -29.5 70.5t-70.5 29.5h-100q-41 0 -70.5 -29.5t-29.5 -70.5v-400zM500 0v1000q0 41 29.5 70.5t70.5 29.5h100q41 0 70.5 -29.5t29.5 -70.5v-1000h-300zM900 0v700q0 41 29.5 70.5t70.5 29.5h100q41 0 70.5 -29.5t29.5 -70.5v-700h-300z' /%3E %3Cglyph unicode='&%23xe186;' d='M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v300h-200v100h200v100h-300v-300h200v-100h-200v-100zM600 300h200v100h100v300h-100v100h-200v-500 zM700 400v300h100v-300h-100z' /%3E %3Cglyph unicode='&%23xe187;' d='M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h100v200h100v-200h100v500h-100v-200h-100v200h-100v-500zM600 300h200v100h100v300h-100v100h-200v-500 zM700 400v300h100v-300h-100z' /%3E %3Cglyph unicode='&%23xe188;' d='M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v100h-200v300h200v100h-300v-500zM600 300h300v100h-200v300h200v100h-300v-500z' /%3E %3Cglyph unicode='&%23xe189;' d='M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 550l300 -150v300zM600 400l300 150l-300 150v-300z' /%3E %3Cglyph unicode='&%23xe190;' d='M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300v500h700v-500h-700zM300 400h130q41 0 68 42t27 107t-28.5 108t-66.5 43h-130v-300zM575 549 q0 -65 27 -107t68 -42h130v300h-130q-38 0 -66.5 -43t-28.5 -108z' /%3E %3Cglyph unicode='&%23xe191;' d='M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v300h-200v100h200v100h-300v-300h200v-100h-200v-100zM601 300h100v100h-100v-100zM700 700h100 v-400h100v500h-200v-100z' /%3E %3Cglyph unicode='&%23xe192;' d='M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v400h-200v100h-100v-500zM301 400v200h100v-200h-100zM601 300h100v100h-100v-100zM700 700h100 v-400h100v500h-200v-100z' /%3E %3Cglyph unicode='&%23xe193;' d='M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 700v100h300v-300h-99v-100h-100v100h99v200h-200zM201 300v100h100v-100h-100zM601 300v100h100v-100h-100z M700 700v100h200v-500h-100v400h-100z' /%3E %3Cglyph unicode='&%23xe194;' d='M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM400 500v200 l100 100h300v-100h-300v-200h300v-100h-300z' /%3E %3Cglyph unicode='&%23xe195;' d='M0 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM182 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM400 400v400h300 l100 -100v-100h-100v100h-200v-100h200v-100h-200v-100h-100zM700 400v100h100v-100h-100z' /%3E %3Cglyph unicode='&%23xe197;' d='M-14 494q0 -80 56.5 -137t135.5 -57h222v300h400v-300h128q120 0 205 86.5t85 207.5t-85 207t-205 86q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5zM300 200h200v300h200v-300h200 l-300 -300z' /%3E %3Cglyph unicode='&%23xe198;' d='M-14 494q0 -80 56.5 -137t135.5 -57h8l414 414l403 -403q94 26 154.5 104.5t60.5 178.5q0 120 -85 206.5t-205 86.5q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5zM300 200l300 300 l300 -300h-200v-300h-200v300h-200z' /%3E %3Cglyph unicode='&%23xe199;' d='M100 200h400v-155l-75 -45h350l-75 45v155h400l-270 300h170l-270 300h170l-300 333l-300 -333h170l-270 -300h170z' /%3E %3Cglyph unicode='&%23xe200;' d='M121 700q0 -53 28.5 -97t75.5 -65q-4 -16 -4 -38q0 -74 52.5 -126.5t126.5 -52.5q56 0 100 30v-306l-75 -45h350l-75 45v306q46 -30 100 -30q74 0 126.5 52.5t52.5 126.5q0 24 -9 55q50 32 79.5 83t29.5 112q0 90 -61.5 155.5t-150.5 71.5q-26 89 -99.5 145.5 t-167.5 56.5q-116 0 -197.5 -81.5t-81.5 -197.5q0 -4 1 -11.5t1 -11.5q-14 2 -23 2q-74 0 -126.5 -52.5t-52.5 -126.5z' /%3E %3C/font%3E %3C/defs%3E%3C/svg%3E\"#glyphicons_halflingsregular") format("svg")}.glyphicon{position:relative;top:1px;display:inline-block;font-family:Glyphicons Halflings;font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}*,:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:Helvetica Neue,Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#428bca;text-decoration:none}a:focus,a:hover{color:#2a6496;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail>img,.thumbnail a>img{display:block;width:100%\9;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;width:100%\9;max-width:100%;height:auto;padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}cite{font-style:normal}.mark,mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#428bca}a.text-primary:hover{color:#3071a9}.text-success{color:#3c763d}a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#428bca}a.bg-primary:hover{background-color:#3071a9}.bg-success{background-color:#dff0d8}a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-inline,.list-unstyled{padding-left:0;list-style:none}.list-inline{margin-left:-5px}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dd,dt{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:"\2014 \00A0"}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:""}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:"\00A0 \2014"}blockquote:after,blockquote:before{content:""}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,Courier New,monospace}code{color:#c7254e;background-color:#f9f2f4;border-radius:4px}code,kbd{padding:2px 4px;font-size:90%}kbd{color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12,.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12,.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12,.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered,.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-child(odd)>td,.table-striped>tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover>tbody>tr:hover>td,.table-hover>tbody>tr:hover>th{background-color:#f5f5f5}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{min-width:0;margin:0}fieldset,legend{padding:0;border:0}legend{display:block;width:100%;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{padding-top:7px}.form-control,output{display:block;font-size:14px;line-height:1.42857143;color:#555}.form-control{width:100%;height:34px;padding:6px 12px;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color .15s ease-in-out,-webkit-box-shadow .15s ease-in-out;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#777;opacity:1}.form-control:-ms-input-placeholder{color:#777}.form-control::-webkit-input-placeholder{color:#777}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#eee;opacity:1}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{line-height:34px;line-height:1.42857143 \0}input[type=date].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm,input[type=time].input-sm{line-height:30px}input[type=date].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg,input[type=time].input-lg{line-height:46px}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;min-height:20px;margin-top:10px;margin-bottom:10px}.checkbox label,.radio label{padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox-inline input[type=checkbox],.checkbox input[type=checkbox],.radio-inline input[type=radio],.radio input[type=radio]{position:absolute;margin-top:4px\9;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}.checkbox-inline.disabled,.checkbox.disabled label,.radio-inline.disabled,.radio.disabled label,fieldset[disabled] .checkbox-inline,fieldset[disabled] .checkbox label,fieldset[disabled] .radio-inline,fieldset[disabled] .radio label,fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}.form-control-static{padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.form-horizontal .form-group-sm .form-control,.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-horizontal .form-group-lg .form-control,.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:25px;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center}.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{top:0;right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:14.3px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.btn.active:focus,.btn:active:focus,.btn:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{pointer-events:none;cursor:not-allowed;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none;opacity:.65}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.active,.btn-default:active,.btn-default:focus,.btn-default:hover,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default.disabled.active,.btn-default.disabled:active,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled],.btn-default[disabled].active,.btn-default[disabled]:active,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default.active,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#428bca;border-color:#357ebd}.btn-primary.active,.btn-primary:active,.btn-primary:focus,.btn-primary:hover,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#3071a9;border-color:#285e8e}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary.disabled.active,.btn-primary.disabled:active,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled],.btn-primary[disabled].active,.btn-primary[disabled]:active,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary.active,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#428bca;border-color:#357ebd}.btn-primary .badge{color:#428bca;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.active,.btn-success:active,.btn-success:focus,.btn-success:hover,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success.disabled.active,.btn-success.disabled:active,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled],.btn-success[disabled].active,.btn-success[disabled]:active,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success.active,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.active,.btn-info:active,.btn-info:focus,.btn-info:hover,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info.disabled.active,.btn-info.disabled:active,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled],.btn-info[disabled].active,.btn-info[disabled]:active,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info.active,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.active,.btn-warning:active,.btn-warning:focus,.btn-warning:hover,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning.disabled.active,.btn-warning.disabled:active,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled],.btn-warning[disabled].active,.btn-warning[disabled]:active,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning.active,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.active,.btn-danger:active,.btn-danger:focus,.btn-danger:hover,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger.disabled.active,.btn-danger.disabled:active,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled],.btn-danger[disabled].active,.btn-danger[disabled]:active,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger.active,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#428bca;cursor:pointer;border-radius:0}.btn-link,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#2a6496;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;-o-transition:height .35s ease;transition:height .35s ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px solid;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#428bca;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px solid}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group-vertical>.btn:focus,.btn-group>.btn:focus{outline:0}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn>input[type=checkbox],[data-toggle=buttons]>.btn>input[type=radio]{position:absolute;z-index:-1;filter:alpha(opacity=0);opacity:0}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group .form-control:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group .form-control:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{font-size:0;white-space:nowrap}.input-group-btn,.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li,.nav>li>a{position:relative;display:block}.nav>li>a{padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#428bca}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid;border-color:#ddd #ddd transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#428bca}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;-webkit-overflow-scrolling:touch;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 hsla(0,0%,100%,.1);box-shadow:inset 0 1px 0 hsla(0,0%,100%,.1)}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:340px}@media (max-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030;-webkit-transform:translateZ(0);-o-transform:translateZ(0);transform:translateZ(0)}@media (min-width:768px){.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;height:50px;padding:15px;font-size:18px;line-height:20px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}@media (min-width:768px){.navbar>.container-fluid .navbar-brand,.navbar>.container .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}.navbar-nav.navbar-right:last-child{margin-right:-15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important}}.navbar-form{padding:10px 15px;margin:8px -15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 hsla(0,0%,100%,.1),0 1px 0 hsla(0,0%,100%,.1);box-shadow:inset 0 1px 0 hsla(0,0%,100%,.1),0 1px 0 hsla(0,0%,100%,.1)}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-form.navbar-right:last-child{margin-right:-15px}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}.navbar-text.navbar-right:last-child{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-nav>li>a,.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#777}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>li>a,.navbar-inverse .navbar-text{color:#777}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#777}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#777}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#428bca;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{color:#2a6496;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:2;color:#fff;cursor:default;background-color:#428bca;border-color:#428bca}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#428bca}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#3071a9}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.nav-pills>.active>a>.badge,a.list-group-item.active>.badge{color:#428bca;background-color:#fff}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding:30px;margin-bottom:30px;background-color:#eee}.jumbotron,.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron{border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron{padding-right:60px;padding-left:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.thumbnail>img,.thumbnail a>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#428bca}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{0%{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{0%{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{0%{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#428bca;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,hsla(0,0%,100%,.15) 25%,transparent 0,transparent 50%,hsla(0,0%,100%,.15) 0,hsla(0,0%,100%,.15) 75%,transparent 0,transparent);background-image:-o-linear-gradient(45deg,hsla(0,0%,100%,.15) 25%,transparent 25%,transparent 50%,hsla(0,0%,100%,.15) 50%,hsla(0,0%,100%,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,hsla(0,0%,100%,.15) 25%,transparent 0,transparent 50%,hsla(0,0%,100%,.15) 0,hsla(0,0%,100%,.15) 75%,transparent 0,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar[aria-valuenow="1"],.progress-bar[aria-valuenow="2"]{min-width:30px}.progress-bar[aria-valuenow="0"]{min-width:30px;color:#777;background-color:transparent;background-image:none;-webkit-box-shadow:none;box-shadow:none}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,hsla(0,0%,100%,.15) 25%,transparent 0,transparent 50%,hsla(0,0%,100%,.15) 0,hsla(0,0%,100%,.15) 75%,transparent 0,transparent);background-image:-o-linear-gradient(45deg,hsla(0,0%,100%,.15) 25%,transparent 25%,transparent 50%,hsla(0,0%,100%,.15) 50%,hsla(0,0%,100%,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,hsla(0,0%,100%,.15) 25%,transparent 0,transparent 50%,hsla(0,0%,100%,.15) 0,hsla(0,0%,100%,.15) 75%,transparent 0,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,hsla(0,0%,100%,.15) 25%,transparent 0,transparent 50%,hsla(0,0%,100%,.15) 0,hsla(0,0%,100%,.15) 75%,transparent 0,transparent);background-image:-o-linear-gradient(45deg,hsla(0,0%,100%,.15) 25%,transparent 25%,transparent 50%,hsla(0,0%,100%,.15) 50%,hsla(0,0%,100%,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,hsla(0,0%,100%,.15) 25%,transparent 0,transparent 50%,hsla(0,0%,100%,.15) 0,hsla(0,0%,100%,.15) 75%,transparent 0,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,hsla(0,0%,100%,.15) 25%,transparent 0,transparent 50%,hsla(0,0%,100%,.15) 0,hsla(0,0%,100%,.15) 75%,transparent 0,transparent);background-image:-o-linear-gradient(45deg,hsla(0,0%,100%,.15) 25%,transparent 25%,transparent 50%,hsla(0,0%,100%,.15) 50%,hsla(0,0%,100%,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,hsla(0,0%,100%,.15) 25%,transparent 0,transparent 50%,hsla(0,0%,100%,.15) 0,hsla(0,0%,100%,.15) 75%,transparent 0,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,hsla(0,0%,100%,.15) 25%,transparent 0,transparent 50%,hsla(0,0%,100%,.15) 0,hsla(0,0%,100%,.15) 75%,transparent 0,transparent);background-image:-o-linear-gradient(45deg,hsla(0,0%,100%,.15) 25%,transparent 25%,transparent 50%,hsla(0,0%,100%,.15) 50%,hsla(0,0%,100%,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,hsla(0,0%,100%,.15) 25%,transparent 0,transparent 50%,hsla(0,0%,100%,.15) 0,hsla(0,0%,100%,.15) 75%,transparent 0,transparent)}.media,.media-body{overflow:hidden;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}a.list-group-item{color:#555}a.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#777;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#e1edf7}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle,.panel-title{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px}.panel-title>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group{margin-bottom:0}.panel>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.list-group+.panel-footer,.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#428bca}.panel-primary>.panel-heading{color:#fff;background-color:#428bca;border-color:#428bca}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#428bca}.panel-primary>.panel-heading .badge{color:#428bca;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#428bca}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{-webkit-appearance:none;padding:0;cursor:pointer;background:0 0;border:0}.modal,.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;-webkit-transform:translate3d(0,-25%,0);-o-transform:translate3d(0,-25%,0);transform:translate3d(0,-25%,0)}.modal.in .modal-dialog{-webkit-transform:translateZ(0);-o-transform:translateZ(0);transform:translateZ(0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5)}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{min-height:16.43px;padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-size:12px;line-height:1.4;visibility:visible;filter:alpha(opacity=0);opacity:0}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{bottom:0;left:5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{right:5px;bottom:0;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;left:5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;right:5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;text-align:left;white-space:normal;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;font-weight:400;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.carousel,.carousel-inner{position:relative}.carousel-inner{width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:left .6s ease-in-out;-o-transition:.6s ease-in-out left;transition:left .6s ease-in-out}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5),rgba(0,0,0,.0001));background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(90deg,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001));filter:progid:DXImageTransform.Microsoft.gradient(startColorstr="#80000000",endColorstr="#00000000",GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001),rgba(0,0,0,.5));background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(90deg,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5));filter:progid:DXImageTransform.Microsoft.gradient(startColorstr="#00000000",endColorstr="#80000000",GradientType=1);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;filter:alpha(opacity=90);outline:0;opacity:.9}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;margin-top:-10px;font-family:serif}.carousel-control .icon-prev:before{content:"\2039"}.carousel-control .icon-next:before{content:"\203a"}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000\9;background-color:transparent;border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-15px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-15px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-15px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{display:table;content:" "}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important;visibility:hidden!important}.affix{position:fixed;-webkit-transform:translateZ(0);-o-transform:translateZ(0);transform:translateZ(0)}@-ms-viewport{width:device-width}.visible-lg,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}} +/*! + * Bootstrap-select v1.7.5 (http://silviomoreto.github.io/bootstrap-select) + * + * Copyright 2013-2015 bootstrap-select + * Licensed under MIT (https://github.com/silviomoreto/bootstrap-select/blob/master/LICENSE) + */.bootstrap-select{width:220px \0}.bootstrap-select>.dropdown-toggle{width:100%;padding-right:25px}.error .bootstrap-select .dropdown-toggle,.has-error .bootstrap-select .dropdown-toggle{border-color:#b94a48}.bootstrap-select.fit-width{width:auto!important}.bootstrap-select:not([class*=col-]):not([class*=form-control]):not(.input-group-btn){width:220px}.bootstrap-select .dropdown-toggle:focus{outline:thin dotted #333!important;outline:5px auto -webkit-focus-ring-color!important;outline-offset:-2px}.bootstrap-select.form-control{margin-bottom:0;padding:0;border:none}.bootstrap-select.form-control:not([class*=col-]){width:100%}.bootstrap-select.form-control.input-group-btn{z-index:auto}.bootstrap-select.btn-group:not(.input-group-btn),.bootstrap-select.btn-group[class*=col-]{float:none;display:inline-block;margin-left:0}.bootstrap-select.btn-group.dropdown-menu-right,.bootstrap-select.btn-group[class*=col-].dropdown-menu-right,.row .bootstrap-select.btn-group[class*=col-].dropdown-menu-right{float:right}.form-group .bootstrap-select.btn-group,.form-horizontal .bootstrap-select.btn-group,.form-inline .bootstrap-select.btn-group{margin-bottom:0}.form-group-lg .bootstrap-select.btn-group.form-control,.form-group-sm .bootstrap-select.btn-group.form-control{padding:0}.form-inline .bootstrap-select.btn-group .form-control{width:100%}.bootstrap-select.btn-group.disabled,.bootstrap-select.btn-group>.disabled{cursor:not-allowed}.bootstrap-select.btn-group.disabled:focus,.bootstrap-select.btn-group>.disabled:focus{outline:0!important}.bootstrap-select.btn-group.bs-container{position:absolute}.bootstrap-select.btn-group.bs-container .dropdown-menu{z-index:1060}.bootstrap-select.btn-group .dropdown-toggle .filter-option{display:inline-block;overflow:hidden;width:100%;text-align:left}.bootstrap-select.btn-group .dropdown-toggle .caret{position:absolute;top:50%;right:12px;margin-top:-2px;vertical-align:middle}.bootstrap-select.btn-group[class*=col-] .dropdown-toggle{width:100%}.bootstrap-select.btn-group .dropdown-menu{min-width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bootstrap-select.btn-group .dropdown-menu.inner{position:static;float:none;border:0;padding:0;margin:0;border-radius:0;-webkit-box-shadow:none;box-shadow:none}.bootstrap-select.btn-group .dropdown-menu li{position:relative}.bootstrap-select.btn-group .dropdown-menu li.active small{color:#fff}.bootstrap-select.btn-group .dropdown-menu li.disabled a{cursor:not-allowed}.bootstrap-select.btn-group .dropdown-menu li a{cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.bootstrap-select.btn-group .dropdown-menu li a.opt{position:relative;padding-left:2.25em}.bootstrap-select.btn-group .dropdown-menu li a span.check-mark{display:none}.bootstrap-select.btn-group .dropdown-menu li a span.text{display:inline-block}.bootstrap-select.btn-group .dropdown-menu li small{padding-left:.5em}.bootstrap-select.btn-group .dropdown-menu .notify{position:absolute;bottom:5px;width:96%;margin:0 2%;min-height:26px;padding:3px 5px;background:#f5f5f5;border:1px solid #e3e3e3;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05);pointer-events:none;opacity:.9;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bootstrap-select.btn-group .no-results{padding:3px;background:#f5f5f5;margin:0 5px;white-space:nowrap}.bootstrap-select.btn-group.fit-width .dropdown-toggle .filter-option{position:static}.bootstrap-select.btn-group.fit-width .dropdown-toggle .caret{position:static;top:auto;margin-top:-1px}.bootstrap-select.btn-group.show-tick .dropdown-menu li.selected a span.check-mark{position:absolute;display:inline-block;right:15px;margin-top:5px}.bootstrap-select.btn-group.show-tick .dropdown-menu li a span.text{margin-right:34px}.bootstrap-select.show-menu-arrow.open>.dropdown-toggle{z-index:1061}.bootstrap-select.show-menu-arrow .dropdown-toggle:before{content:"";border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid hsla(0,0%,80%,.2);position:absolute;bottom:-4px;left:9px;display:none}.bootstrap-select.show-menu-arrow .dropdown-toggle:after{content:"";border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #fff;position:absolute;bottom:-4px;left:10px;display:none}.bootstrap-select.show-menu-arrow.dropup .dropdown-toggle:before{bottom:auto;top:-3px;border-top:7px solid hsla(0,0%,80%,.2);border-bottom:0}.bootstrap-select.show-menu-arrow.dropup .dropdown-toggle:after{bottom:auto;top:-3px;border-top:6px solid #fff;border-bottom:0}.bootstrap-select.show-menu-arrow.pull-right .dropdown-toggle:before{right:12px;left:auto}.bootstrap-select.show-menu-arrow.pull-right .dropdown-toggle:after{right:13px;left:auto}.bootstrap-select.show-menu-arrow.open>.dropdown-toggle:after,.bootstrap-select.show-menu-arrow.open>.dropdown-toggle:before{display:block}.bs-actionsbox,.bs-donebutton,.bs-searchbox{padding:4px 8px}.bs-actionsbox{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bs-actionsbox .btn-group button{width:50%}.bs-donebutton{float:left;width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bs-donebutton .btn-group button{width:100%}.bs-searchbox+.bs-actionsbox{padding:0 8px 4px}.bs-searchbox .form-control{margin-bottom:0;width:100%;float:none}select.bs-select-hidden,select.selectpicker{display:none!important}select.mobile-device{position:absolute!important;top:0;left:0;display:block!important;width:100%;height:100%!important;opacity:0} +/*! + * bootstrap-tokenfield + * https://github.com/sliptree/bootstrap-tokenfield + * Copyright 2013-2014 Sliptree and other contributors; Licensed MIT + */@-webkit-keyframes "blink"{0%{border-color:#ededed}to{border-color:#b94a48}}@-moz-keyframes "blink"{0%{border-color:#ededed}to{border-color:#b94a48}}@keyframes "blink"{0%{border-color:#ededed}to{border-color:#b94a48}}.tokenfield{height:auto;min-height:34px;padding-bottom:0}.tokenfield.focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.tokenfield .token{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;display:inline-block;border:1px solid #d9d9d9;background-color:#ededed;white-space:nowrap;margin:-1px 5px 5px 0;height:22px;vertical-align:top;cursor:default}.tokenfield .token:hover{border-color:#b9b9b9}.tokenfield .token.active{border-color:#52a8ec;border-color:rgba(82,168,236,.8)}.tokenfield .token.duplicate{border-color:#ebccd1;-webkit-animation-name:blink;animation-name:blink;-webkit-animation-duration:.1s;animation-duration:.1s;-webkit-animation-direction:normal;animation-direction:normal;-webkit-animation-timing-function:ease;animation-timing-function:ease;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.tokenfield .token.invalid{background:0 0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;border:1px solid transparent;border-bottom:1px dotted #d9534f}.tokenfield .token.invalid.active{background:#ededed;border:1px solid #ededed;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.tokenfield .token .token-label{display:inline-block;overflow:hidden;text-overflow:ellipsis;padding-left:4px;vertical-align:top}.tokenfield .token .close{font-family:Arial;display:inline-block;line-height:100%;font-size:1.1em;line-height:1.49em;margin-left:5px;float:none;height:100%;vertical-align:top;padding-right:4px}.tokenfield .token-input{background:0 0;width:60px;min-width:60px;border:0;height:20px;padding:0;margin-bottom:6px;-webkit-box-shadow:none;box-shadow:none}.tokenfield .token-input:focus{border-color:transparent;outline:0;-webkit-box-shadow:none;box-shadow:none}.tokenfield.disabled{cursor:not-allowed;background-color:#eee}.tokenfield.disabled .token-input{cursor:not-allowed}.tokenfield.disabled .token:hover{cursor:not-allowed;border-color:#d9d9d9}.tokenfield.disabled .token:hover .close{cursor:not-allowed;opacity:.2;filter:alpha(opacity=20)}.has-warning .tokenfield.focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-error .tokenfield.focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-success .tokenfield.focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.input-group-sm .tokenfield,.tokenfield.input-sm{min-height:30px;padding-bottom:0}.input-group-sm .token,.tokenfield.input-sm .token{height:20px;margin-bottom:4px}.input-group-sm .token-input,.tokenfield.input-sm .token-input{height:18px;margin-bottom:5px}.input-group-lg .tokenfield,.tokenfield.input-lg{min-height:45px;padding-bottom:4px}.input-group-lg .token,.tokenfield.input-lg .token{height:25px}.input-group-lg .token-label,.tokenfield.input-lg .token-label{line-height:23px}.input-group-lg .token .close,.tokenfield.input-lg .token .close{line-height:1.3em}.input-group-lg .token-input,.tokenfield.input-lg .token-input{height:23px;line-height:23px;margin-bottom:6px;vertical-align:top}.tokenfield.rtl{direction:rtl;text-align:right}.tokenfield.rtl .token{margin:-1px 0 5px 5px}.tokenfield.rtl .token .token-label{padding-left:0;padding-right:4px}html{position:relative;min-height:100%}body{margin-bottom:130px}body>.container{padding:75px 15px 0}.btn:focus{outline:none!important}.navbar-link{text-decoration:none!important}.status-text{font-weight:700;font-size:11px!important;margin:0}@media screen and (min-width:1024px){.modal.modal-narrow .modal-dialog{width:30%}.modal-narrow .modal-body{overflow-y:auto}}.progress-text{padding-top:3px;font-size:11px!important;color:#555;white-space:nowrap;overflow:hidden;display:block;text-overflow:ellipsis}.navbar>.container{margin-bottom:-10px}.alert-small{width:300px}.strong{font-weight:700}.popover-content{padding:9px 12px 3px}.panel-sm{margin-bottom:6px}.panel-sm>.panel-heading{font-weight:700;font-size:14px;padding:5px 10px}.panel-sm>.panel-body{font-size:12px;line-height:1.4;padding:10px}.status-light{width:10px;height:10px;text-align:center;padding:1px 0;border-radius:15px;margin-bottom:3px;margin-right:-3px}#folder{max-height:345px;overflow-y:auto}#folder>table{font-size:80%}.sorted{color:maroon}#composer,#message_view{display:none;font-size:80%}.modal-header .btn-group{margin-right:5px;margin-top:-5px}.input-group-sm .tokenfield{height:auto}.modal-header .btn-group button,.modal-header .btn-group button:hover{color:#fff}.primary.modal-header{background-color:#337ab7;color:#fff}.primay.modal-body{padding:0;margin-left:5px}.primary.modal-footer{background-color:#333}#composer_attachments{margin-top:10px}#body{border:none;min-height:50px;margin:3px}#body,#msg_body{white-space:pre-wrap}#msg_body{min-height:150px}.panel-title{font-size:115%;font-weight:bolder}@media screen and (min-width:1024px){#body,#msg_body{min-height:400px;max-height:600px;overflow-y:auto!important;white-space:pre-wrap}#message_view{font-size:80%}#folder{max-height:700px;overflow-y:auto}}.panel-fluid{margin:0;padding:0}.panel-heading p{margin:0}#connectURLPreview{font-size:80%}.footer{position:absolute;bottom:15px;width:100%;height:120px}#console{height:120px;overflow-y:auto;background-color:#272822;color:#fff;line-height:.5em;padding:3px}#console .terminal{font-size:.6em}.msgbody blockquote{font-size:1em;color:#777;margin-top:0;padding-left:5px;margin-bottom:10px}.msgbody blockquote p{margin-top:0;margin-bottom:0}.msgbody blockquote ul{margin-top:0;margin-bottom:-20px;margin-left:0;padding-left:15px}@media screen and (min-width:1024px){.footer{position:absolute;bottom:15px;width:100%;height:130px}#console{height:120px;overflow-y:auto;background-color:#272822;color:#fff;line-height:1em;padding:3px}#console .terminal{font-size:.9em}}.btn-file{position:relative;overflow:hidden}.btn-file input[type=file]{position:absolute;top:0;right:0;min-width:100%;min-height:100%;font-size:100px;text-align:right;filter:alpha(opacity=0);opacity:0;outline:none;background:#fff;cursor:inherit;display:block}.compose-options{margin-top:10px;margin-bottom:-10px}#composer_error{font-size:12px;margin:5px 0;padding:5px}input[type=checkbox],input[type=radio]{vertical-align:-2px;margin:0;padding:0}.table-fixed{overflow-y:auto;height:400px}.table-fixed thead th{position:sticky;top:0;background:#eee} \ No newline at end of file diff --git a/web/res/bootstrap-3.2.0-dist/fonts/glyphicons-halflings-regular.eot b/web/dist/fonts/glyphicons-halflings-regular.eot similarity index 100% rename from web/res/bootstrap-3.2.0-dist/fonts/glyphicons-halflings-regular.eot rename to web/dist/fonts/glyphicons-halflings-regular.eot diff --git a/web/res/bootstrap-3.2.0-dist/fonts/glyphicons-halflings-regular.ttf b/web/dist/fonts/glyphicons-halflings-regular.ttf similarity index 100% rename from web/res/bootstrap-3.2.0-dist/fonts/glyphicons-halflings-regular.ttf rename to web/dist/fonts/glyphicons-halflings-regular.ttf diff --git a/web/res/bootstrap-3.2.0-dist/fonts/glyphicons-halflings-regular.woff b/web/dist/fonts/glyphicons-halflings-regular.woff similarity index 100% rename from web/res/bootstrap-3.2.0-dist/fonts/glyphicons-halflings-regular.woff rename to web/dist/fonts/glyphicons-halflings-regular.woff diff --git a/web/res/tmpl/index.html b/web/dist/index.html similarity index 92% rename from web/res/tmpl/index.html rename to web/dist/index.html index d34044c4..0e55c965 100644 --- a/web/res/tmpl/index.html +++ b/web/dist/index.html @@ -9,10 +9,7 @@ {{.AppName}} - Mailbox - - - - + @@ -154,12 +151,7 @@ @@ -187,7 +179,7 @@ Connect to remote node... - + Connect to remote node... data-width="100%" id="transportSelect" > - - ARDOP - AX.25 - Pactor - serial-tnc - telnet - WINMOR + ARDOP + AX.25 + PACTOR + Telnet + VARA FM + VARA HF + + AX.25+agwpe + AX.25+linux + AX.25+serial-tnc @@ -227,10 +222,10 @@ Connect to remote node... /> - + freq: - + Connect to remote node... + + + bandwidth: + + + + + address: - + Connect to remote node... ARDOP Packet Pactor - WINMOR + VARA FM + VARA HF @@ -433,7 +441,7 @@ Update Form Templates Current forms version: unknown - + Update now @@ -467,7 +475,7 @@ About {{.AppName}} {{.Version}} - + - - - - - - - - - - +
{{.AppName}} {{.Version}}