diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 00000000..fedf55d1
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,33 @@
+admin/admin
+checker/checker
+dashboard/dashboard
+evdist/evdist
+generator/generator
+receiver/receiver
+repochecker/repochecker
+frontend/fic/build
+frontend/fic/node_modules
+qa/ui/build
+qa/ui/node_modules
+fickit-backend-initrd.img
+fickit-backend-kernel
+fickit-backend-squashfs.img
+fickit-backend-state
+fickit-frontend-initrd.img
+fickit-frontend-kernel
+fickit-frontend-squashfs.img
+fickit-frontend-state
+fickit-prepare-initrd.img
+fickit-prepare-kernel
+fickit-update-initrd.img
+fickit-update-kernel
+DASHBOARD
+FILES
+PKI
+REMOTE
+repochecker/*.so
+SETTINGS
+SETTINGSDIST
+submissions
+TEAMS
+vendor
\ No newline at end of file
diff --git a/.drone-manifest-fic-admin.yml b/.drone-manifest-fic-admin.yml
new file mode 100644
index 00000000..fc1154f7
--- /dev/null
+++ b/.drone-manifest-fic-admin.yml
@@ -0,0 +1,22 @@
+image: nemunaire/fic-admin:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
+{{#if build.tags}}
+tags:
+{{#each build.tags}}
+ - {{this}}
+{{/each}}
+{{/if}}
+manifests:
+ - image: nemunaire/fic-admin:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
+ platform:
+ architecture: amd64
+ os: linux
+ - image: nemunaire/fic-admin:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
+ platform:
+ architecture: arm64
+ os: linux
+ variant: v8
+ - image: nemunaire/fic-admin:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
+ platform:
+ architecture: arm
+ os: linux
+ variant: v7
diff --git a/.drone-manifest-fic-checker.yml b/.drone-manifest-fic-checker.yml
new file mode 100644
index 00000000..13721117
--- /dev/null
+++ b/.drone-manifest-fic-checker.yml
@@ -0,0 +1,22 @@
+image: nemunaire/fic-checker:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
+{{#if build.tags}}
+tags:
+{{#each build.tags}}
+ - {{this}}
+{{/each}}
+{{/if}}
+manifests:
+ - image: nemunaire/fic-checker:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
+ platform:
+ architecture: amd64
+ os: linux
+ - image: nemunaire/fic-checker:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
+ platform:
+ architecture: arm64
+ os: linux
+ variant: v8
+ - image: nemunaire/fic-checker:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
+ platform:
+ architecture: arm
+ os: linux
+ variant: v7
diff --git a/.drone-manifest-fic-dashboard.yml b/.drone-manifest-fic-dashboard.yml
new file mode 100644
index 00000000..4f74d234
--- /dev/null
+++ b/.drone-manifest-fic-dashboard.yml
@@ -0,0 +1,22 @@
+image: nemunaire/fic-dashboard:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
+{{#if build.tags}}
+tags:
+{{#each build.tags}}
+ - {{this}}
+{{/each}}
+{{/if}}
+manifests:
+ - image: nemunaire/fic-dashboard:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
+ platform:
+ architecture: amd64
+ os: linux
+ - image: nemunaire/fic-dashboard:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
+ platform:
+ architecture: arm64
+ os: linux
+ variant: v8
+ - image: nemunaire/fic-dashboard:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
+ platform:
+ architecture: arm
+ os: linux
+ variant: v7
diff --git a/.drone-manifest-fic-evdist.yml b/.drone-manifest-fic-evdist.yml
new file mode 100644
index 00000000..70b1e5c5
--- /dev/null
+++ b/.drone-manifest-fic-evdist.yml
@@ -0,0 +1,22 @@
+image: nemunaire/fic-evdist:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
+{{#if build.tags}}
+tags:
+{{#each build.tags}}
+ - {{this}}
+{{/each}}
+{{/if}}
+manifests:
+ - image: nemunaire/fic-evdist:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
+ platform:
+ architecture: amd64
+ os: linux
+ - image: nemunaire/fic-evdist:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
+ platform:
+ architecture: arm64
+ os: linux
+ variant: v8
+ - image: nemunaire/fic-evdist:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
+ platform:
+ architecture: arm
+ os: linux
+ variant: v7
diff --git a/.drone-manifest-fic-frontend-ui.yml b/.drone-manifest-fic-frontend-ui.yml
new file mode 100644
index 00000000..0fc4a3a6
--- /dev/null
+++ b/.drone-manifest-fic-frontend-ui.yml
@@ -0,0 +1,22 @@
+image: nemunaire/fic-frontend-ui:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
+{{#if build.tags}}
+tags:
+{{#each build.tags}}
+ - {{this}}
+{{/each}}
+{{/if}}
+manifests:
+ - image: nemunaire/fic-frontend-ui:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
+ platform:
+ architecture: amd64
+ os: linux
+ - image: nemunaire/fic-frontend-ui:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
+ platform:
+ architecture: arm64
+ os: linux
+ variant: v8
+ - image: nemunaire/fic-frontend-ui:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
+ platform:
+ architecture: arm
+ os: linux
+ variant: v7
diff --git a/.drone-manifest-fic-generator.yml b/.drone-manifest-fic-generator.yml
new file mode 100644
index 00000000..a2b2443f
--- /dev/null
+++ b/.drone-manifest-fic-generator.yml
@@ -0,0 +1,22 @@
+image: nemunaire/fic-generator:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
+{{#if build.tags}}
+tags:
+{{#each build.tags}}
+ - {{this}}
+{{/each}}
+{{/if}}
+manifests:
+ - image: nemunaire/fic-generator:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
+ platform:
+ architecture: amd64
+ os: linux
+ - image: nemunaire/fic-generator:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
+ platform:
+ architecture: arm64
+ os: linux
+ variant: v8
+ - image: nemunaire/fic-generator:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
+ platform:
+ architecture: arm
+ os: linux
+ variant: v7
diff --git a/.drone-manifest-fic-get-remote-files.yml b/.drone-manifest-fic-get-remote-files.yml
new file mode 100644
index 00000000..0f64b8c1
--- /dev/null
+++ b/.drone-manifest-fic-get-remote-files.yml
@@ -0,0 +1,22 @@
+image: nemunaire/fic-get-remote-files:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
+{{#if build.tags}}
+tags:
+{{#each build.tags}}
+ - {{this}}
+{{/each}}
+{{/if}}
+manifests:
+ - image: nemunaire/fic-get-remote-files:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
+ platform:
+ architecture: amd64
+ os: linux
+ - image: nemunaire/fic-get-remote-files:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
+ platform:
+ architecture: arm64
+ os: linux
+ variant: v8
+ - image: nemunaire/fic-get-remote-files:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
+ platform:
+ architecture: arm
+ os: linux
+ variant: v7
diff --git a/.drone-manifest-fic-nginx.yml b/.drone-manifest-fic-nginx.yml
new file mode 100644
index 00000000..cbd2dcce
--- /dev/null
+++ b/.drone-manifest-fic-nginx.yml
@@ -0,0 +1,22 @@
+image: nemunaire/fic-nginx:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
+{{#if build.tags}}
+tags:
+{{#each build.tags}}
+ - {{this}}
+{{/each}}
+{{/if}}
+manifests:
+ - image: nemunaire/fic-nginx:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
+ platform:
+ architecture: amd64
+ os: linux
+ - image: nemunaire/fic-nginx:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
+ platform:
+ architecture: arm64
+ os: linux
+ variant: v8
+ - image: nemunaire/fic-nginx:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
+ platform:
+ architecture: arm
+ os: linux
+ variant: v7
diff --git a/.drone-manifest-fic-qa.yml b/.drone-manifest-fic-qa.yml
new file mode 100644
index 00000000..5a158970
--- /dev/null
+++ b/.drone-manifest-fic-qa.yml
@@ -0,0 +1,22 @@
+image: nemunaire/fic-qa:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
+{{#if build.tags}}
+tags:
+{{#each build.tags}}
+ - {{this}}
+{{/each}}
+{{/if}}
+manifests:
+ - image: nemunaire/fic-qa:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
+ platform:
+ architecture: amd64
+ os: linux
+ - image: nemunaire/fic-qa:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
+ platform:
+ architecture: arm64
+ os: linux
+ variant: v8
+ - image: nemunaire/fic-qa:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
+ platform:
+ architecture: arm
+ os: linux
+ variant: v7
diff --git a/.drone-manifest-fic-receiver.yml b/.drone-manifest-fic-receiver.yml
new file mode 100644
index 00000000..eda2fe25
--- /dev/null
+++ b/.drone-manifest-fic-receiver.yml
@@ -0,0 +1,22 @@
+image: nemunaire/fic-receiver:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
+{{#if build.tags}}
+tags:
+{{#each build.tags}}
+ - {{this}}
+{{/each}}
+{{/if}}
+manifests:
+ - image: nemunaire/fic-receiver:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
+ platform:
+ architecture: amd64
+ os: linux
+ - image: nemunaire/fic-receiver:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
+ platform:
+ architecture: arm64
+ os: linux
+ variant: v8
+ - image: nemunaire/fic-receiver:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
+ platform:
+ architecture: arm
+ os: linux
+ variant: v7
diff --git a/.drone-manifest-fic-repochecker.yml b/.drone-manifest-fic-repochecker.yml
new file mode 100644
index 00000000..9b239931
--- /dev/null
+++ b/.drone-manifest-fic-repochecker.yml
@@ -0,0 +1,22 @@
+image: nemunaire/fic-repochecker:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
+{{#if build.tags}}
+tags:
+{{#each build.tags}}
+ - {{this}}
+{{/each}}
+{{/if}}
+manifests:
+ - image: nemunaire/fic-repochecker:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
+ platform:
+ architecture: amd64
+ os: linux
+ - image: nemunaire/fic-repochecker:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
+ platform:
+ architecture: arm64
+ os: linux
+ variant: v8
+ - image: nemunaire/fic-repochecker:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
+ platform:
+ architecture: arm
+ os: linux
+ variant: v7
diff --git a/.drone-manifest-fickit-deploy.yml b/.drone-manifest-fickit-deploy.yml
new file mode 100644
index 00000000..bfa37052
--- /dev/null
+++ b/.drone-manifest-fickit-deploy.yml
@@ -0,0 +1,22 @@
+image: nemunaire/fickit-deploy:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
+{{#if build.tags}}
+tags:
+{{#each build.tags}}
+ - {{this}}
+{{/each}}
+{{/if}}
+manifests:
+ - image: nemunaire/fickit-deploy:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
+ platform:
+ architecture: amd64
+ os: linux
+ - image: nemunaire/fickit-deploy:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
+ platform:
+ architecture: arm64
+ os: linux
+ variant: v8
+ - image: nemunaire/fickit-deploy:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
+ platform:
+ architecture: arm
+ os: linux
+ variant: v7
diff --git a/.drone.yml b/.drone.yml
new file mode 100644
index 00000000..3e0e2cbc
--- /dev/null
+++ b/.drone.yml
@@ -0,0 +1,816 @@
+---
+kind: pipeline
+type: docker
+name: build-amd64
+
+platform:
+ os: linux
+ arch: amd64
+
+workspace:
+ base: /go
+ path: src/srs.epita.fr/fic-server
+
+steps:
+ - name: get deps
+ image: golang:alpine
+ commands:
+ - apk --no-cache add git
+ - go get -v -d ./...
+ - mkdir deploy
+
+ - name: build qa ui
+ image: node:23-alpine
+ commands:
+ - cd qa/ui
+ - npm install --network-timeout=100000
+ - npm run build
+ - tar chjf ../../deploy/htdocs-qa.tar.bz2 build
+
+ - name: vet and tests
+ image: golang:alpine
+ commands:
+ - apk --no-cache add build-base
+ - go vet -buildvcs=false -tags gitgo ./...
+ - go vet -buildvcs=false ./...
+ - go test ./...
+
+ - name: build admin
+ image: golang:alpine
+ commands:
+ - go build -buildvcs=false -tags gitgo -o deploy/admin-gitgo-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/admin
+ - go build -buildvcs=false -o deploy/admin-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/admin
+ - tar chjf deploy/htdocs-admin.tar.bz2 htdocs-admin
+ environment:
+ CGO_ENABLED: 0
+ when:
+ branch:
+ exclude:
+ - master
+
+ - name: build checker
+ image: golang:alpine
+ commands:
+ - go build -buildvcs=false -o deploy/checker-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/checker
+ environment:
+ CGO_ENABLED: 0
+ when:
+ branch:
+ exclude:
+ - master
+
+ - name: build evdist
+ image: golang:alpine
+ commands:
+ - go build -buildvcs=false -o deploy/evdist-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/evdist
+ environment:
+ CGO_ENABLED: 0
+ when:
+ branch:
+ exclude:
+ - master
+
+ - name: build generator
+ image: golang:alpine
+ commands:
+ - go build -buildvcs=false -o deploy/generator-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/generator
+ environment:
+ CGO_ENABLED: 0
+ when:
+ branch:
+ exclude:
+ - master
+
+ - name: build receiver
+ image: golang:alpine
+ commands:
+ - go build -buildvcs=false -o deploy/receiver-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/receiver
+ environment:
+ CGO_ENABLED: 0
+ when:
+ branch:
+ exclude:
+ - master
+
+ - name: build frontend fic ui
+ image: node:23-alpine
+ commands:
+ - cd frontend/fic
+ - npm install --network-timeout=100000
+ - npm run build
+ - tar chjf ../../deploy/htdocs-frontend-fic.tar.bz2 build
+ when:
+ branch:
+ exclude:
+ - master
+
+ - name: build dashboard
+ image: golang:alpine
+ commands:
+ - go build -buildvcs=false -o deploy/dashboard-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/dashboard
+ - tar chjf deploy/htdocs-dashboard.tar.bz2 htdocs-dashboard
+ environment:
+ CGO_ENABLED: 0
+ when:
+ branch:
+ exclude:
+ - master
+
+ - name: build repochecker
+ image: golang:alpine
+ commands:
+ - apk --no-cache add build-base
+ - go build -buildvcs=false --tags checkupdate -o deploy/repochecker-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/repochecker
+ - go build -buildvcs=false -buildmode=plugin -o deploy/repochecker-epita-rules-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}.so srs.epita.fr/fic-server/repochecker/epita
+ - go build -buildvcs=false -buildmode=plugin -o deploy/repochecker-file-inspector-rules-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}.so srs.epita.fr/fic-server/repochecker/file-inspector
+ - go build -buildvcs=false -buildmode=plugin -o deploy/repochecker-grammalecte-rules-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}.so srs.epita.fr/fic-server/repochecker/grammalecte
+ - go build -buildvcs=false -buildmode=plugin -o deploy/repochecker-pcap-inspector-rules-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}.so srs.epita.fr/fic-server/repochecker/pcap-inspector
+ - go build -buildvcs=false -buildmode=plugin -o deploy/repochecker-videos-rules-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}.so srs.epita.fr/fic-server/repochecker/videos
+ - grep "const version" repochecker/update.go | sed -r 's/^.*=\s*(\S.*)$/\1/' > deploy/repochecker.version
+ when:
+ branch:
+ exclude:
+ - master
+
+ - name: build qa
+ image: golang:alpine
+ commands:
+ - go build -buildvcs=false -o deploy/qa-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/qa
+ environment:
+ CGO_ENABLED: 0
+ when:
+ branch:
+ exclude:
+ - master
+
+ - name: docker admin
+ image: plugins/docker
+ settings:
+ username:
+ from_secret: docker_username
+ password:
+ from_secret: docker_password
+ repo: nemunaire/fic-admin
+ auto_tag: true
+ auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
+ dockerfile: Dockerfile-admin
+ when:
+ branch:
+ - master
+
+ - name: docker checker
+ image: plugins/docker
+ settings:
+ username:
+ from_secret: docker_username
+ password:
+ from_secret: docker_password
+ repo: nemunaire/fic-checker
+ auto_tag: true
+ auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
+ dockerfile: Dockerfile-checker
+ when:
+ branch:
+ - master
+
+ - name: docker evdist
+ image: plugins/docker
+ settings:
+ username:
+ from_secret: docker_username
+ password:
+ from_secret: docker_password
+ repo: nemunaire/fic-evdist
+ auto_tag: true
+ auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
+ dockerfile: Dockerfile-evdist
+ when:
+ branch:
+ - master
+
+ - name: docker generator
+ image: plugins/docker
+ settings:
+ username:
+ from_secret: docker_username
+ password:
+ from_secret: docker_password
+ repo: nemunaire/fic-generator
+ auto_tag: true
+ auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
+ dockerfile: Dockerfile-generator
+ when:
+ branch:
+ - master
+
+ - name: docker receiver
+ image: plugins/docker
+ settings:
+ username:
+ from_secret: docker_username
+ password:
+ from_secret: docker_password
+ repo: nemunaire/fic-receiver
+ auto_tag: true
+ auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
+ dockerfile: Dockerfile-receiver
+ when:
+ branch:
+ - master
+
+ - name: docker frontend nginx
+ image: plugins/docker
+ settings:
+ username:
+ from_secret: docker_username
+ password:
+ from_secret: docker_password
+ repo: nemunaire/fic-nginx
+ auto_tag: true
+ auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
+ dockerfile: Dockerfile-nginx
+ when:
+ branch:
+ - master
+
+ - name: docker frontend ui
+ image: plugins/docker
+ settings:
+ username:
+ from_secret: docker_username
+ password:
+ from_secret: docker_password
+ repo: nemunaire/fic-frontend-ui
+ auto_tag: true
+ auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
+ dockerfile: Dockerfile-frontend-ui
+ when:
+ branch:
+ - master
+
+ - name: docker dashboard
+ image: plugins/docker
+ settings:
+ username:
+ from_secret: docker_username
+ password:
+ from_secret: docker_password
+ repo: nemunaire/fic-dashboard
+ auto_tag: true
+ auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
+ dockerfile: Dockerfile-dashboard
+ when:
+ branch:
+ - master
+
+ - name: docker qa
+ image: plugins/docker
+ settings:
+ username:
+ from_secret: docker_username
+ password:
+ from_secret: docker_password
+ repo: nemunaire/fic-qa
+ auto_tag: true
+ auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
+ dockerfile: Dockerfile-qa
+ when:
+ branch:
+ - master
+
+ - name: docker repochecker
+ image: plugins/docker
+ settings:
+ username:
+ from_secret: docker_username
+ password:
+ from_secret: docker_password
+ repo: nemunaire/fic-repochecker
+ auto_tag: true
+ auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
+ dockerfile: Dockerfile-repochecker
+ when:
+ branch:
+ - master
+
+ - name: docker remote-scores-sync-zqds
+ image: plugins/docker
+ settings:
+ username:
+ from_secret: docker_username
+ password:
+ from_secret: docker_password
+ repo: nemunaire/fic-remote-scores-sync-zqds
+ auto_tag: true
+ auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
+ dockerfile: Dockerfile-remote-scores-sync-zqds
+ when:
+ branch:
+ - master
+
+ - name: docker remote-challenge-sync-airbus
+ image: plugins/docker
+ settings:
+ username:
+ from_secret: docker_username
+ password:
+ from_secret: docker_password
+ repo: nemunaire/fic-remote-challenge-sync-airbus
+ auto_tag: true
+ auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
+ dockerfile: Dockerfile-remote-challenge-sync-airbus
+ when:
+ branch:
+ - master
+
+ - name: docker fic-get-remote-files
+ failure: ignore
+ image: plugins/docker
+ settings:
+ username:
+ from_secret: docker_username
+ password:
+ from_secret: docker_password
+ repo: nemunaire/fic-get-remote-files
+ auto_tag: true
+ auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
+ dockerfile: Dockerfile-get-remote-files
+ when:
+ branch:
+ - master
+
+ - name: docker fickit-deploy
+ image: plugins/docker
+ settings:
+ username:
+ from_secret: docker_username
+ password:
+ from_secret: docker_password
+ repo: nemunaire/fickit-deploy
+ auto_tag: true
+ auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
+ dockerfile: Dockerfile-deploy
+ when:
+ branch:
+ - master
+
+trigger:
+ event:
+ - cron
+ - push
+ - tag
+
+---
+kind: pipeline
+type: docker
+name: build-arm64
+
+platform:
+ os: linux
+ arch: arm64
+
+workspace:
+ base: /go
+ path: src/srs.epita.fr/fic-server
+
+steps:
+ - name: get deps
+ image: golang:alpine
+ commands:
+ - apk --no-cache add git
+ - go get -d ./...
+ - mkdir deploy
+
+ - name: build admin
+ image: golang:alpine
+ commands:
+ - apk --no-cache add build-base
+ - go build -buildvcs=false -o deploy/admin-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/admin
+ environment:
+ CGO_ENABLED: 0
+ when:
+ branch:
+ exclude:
+ - master
+
+ - name: build checker
+ image: golang:alpine
+ commands:
+ - go build -buildvcs=false -o deploy/checker-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/checker
+ environment:
+ CGO_ENABLED: 0
+ when:
+ branch:
+ exclude:
+ - master
+
+ - name: build evdist
+ image: golang:alpine
+ commands:
+ - go build -buildvcs=false -o deploy/evdist-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/evdist
+ environment:
+ CGO_ENABLED: 0
+ when:
+ branch:
+ exclude:
+ - master
+
+ - name: build generator
+ image: golang:alpine
+ commands:
+ - go build -buildvcs=false -o deploy/generator-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/generator
+ environment:
+ CGO_ENABLED: 0
+ when:
+ branch:
+ exclude:
+ - master
+
+ - name: build receiver
+ image: golang:alpine
+ commands:
+ - go build -buildvcs=false -o deploy/receiver-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/receiver
+ environment:
+ CGO_ENABLED: 0
+ when:
+ branch:
+ exclude:
+ - master
+
+ - name: build frontend fic ui
+ image: node:23-alpine
+ commands:
+ - cd frontend/fic
+ - npm install --network-timeout=100000
+ - npm run build
+ when:
+ branch:
+ exclude:
+ - master
+
+ - name: build dashboard
+ image: golang:alpine
+ commands:
+ - go build -buildvcs=false -o deploy/dashboard-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/dashboard
+ environment:
+ CGO_ENABLED: 0
+ when:
+ branch:
+ exclude:
+ - master
+
+ - name: build repochecker
+ image: golang:alpine
+ commands:
+ - apk --no-cache add build-base
+ - go build -buildvcs=false --tags checkupdate -o deploy/repochecker-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/repochecker
+ environment:
+ CGO_ENABLED: 0
+ when:
+ branch:
+ exclude:
+ - master
+
+ - name: build repochecker for macOS
+ image: golang:alpine
+ commands:
+ - apk --no-cache add build-base
+ - go build -buildvcs=false --tags checkupdate -o deploy/repochecker-darwin-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/repochecker
+ environment:
+ CGO_ENABLED: 0
+ GOOS: darwin
+ GOARCH: arm64
+ when:
+ branch:
+ exclude:
+ - master
+
+ - name: build qa ui
+ image: node:23-alpine
+ commands:
+ - cd qa/ui
+ - npm install --network-timeout=100000
+ - npm run build
+ - tar chjf ../../deploy/htdocs-qa.tar.bz2 build
+ when:
+ branch:
+ exclude:
+ - master
+
+ - name: build qa
+ image: golang:alpine
+ commands:
+ - go build -buildvcs=false -o deploy/qa-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/qa
+ environment:
+ CGO_ENABLED: 0
+ when:
+ branch:
+ exclude:
+ - master
+
+ - name: docker admin
+ image: plugins/docker
+ settings:
+ username:
+ from_secret: docker_username
+ password:
+ from_secret: docker_password
+ repo: nemunaire/fic-admin
+ auto_tag: true
+ auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
+ dockerfile: Dockerfile-admin
+ when:
+ branch:
+ - master
+
+ - name: docker checker
+ image: plugins/docker
+ settings:
+ username:
+ from_secret: docker_username
+ password:
+ from_secret: docker_password
+ repo: nemunaire/fic-checker
+ auto_tag: true
+ auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
+ dockerfile: Dockerfile-checker
+ when:
+ branch:
+ - master
+
+ - name: docker evdist
+ image: plugins/docker
+ settings:
+ username:
+ from_secret: docker_username
+ password:
+ from_secret: docker_password
+ repo: nemunaire/fic-evdist
+ auto_tag: true
+ auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
+ dockerfile: Dockerfile-evdist
+ when:
+ branch:
+ - master
+
+ - name: docker generator
+ image: plugins/docker
+ settings:
+ username:
+ from_secret: docker_username
+ password:
+ from_secret: docker_password
+ repo: nemunaire/fic-generator
+ auto_tag: true
+ auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
+ dockerfile: Dockerfile-generator
+ when:
+ branch:
+ - master
+
+ - name: docker fic-get-remote-files
+ failure: ignore
+ image: plugins/docker
+ settings:
+ username:
+ from_secret: docker_username
+ password:
+ from_secret: docker_password
+ repo: nemunaire/fic-get-remote-files
+ auto_tag: true
+ auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
+ dockerfile: Dockerfile-get-remote-files
+ when:
+ branch:
+ - master
+
+ - name: docker receiver
+ image: plugins/docker
+ settings:
+ username:
+ from_secret: docker_username
+ password:
+ from_secret: docker_password
+ repo: nemunaire/fic-receiver
+ auto_tag: true
+ auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
+ dockerfile: Dockerfile-receiver
+ when:
+ branch:
+ - master
+
+ - name: docker frontend nginx
+ image: plugins/docker
+ settings:
+ username:
+ from_secret: docker_username
+ password:
+ from_secret: docker_password
+ repo: nemunaire/fic-nginx
+ auto_tag: true
+ auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
+ dockerfile: Dockerfile-nginx
+ when:
+ branch:
+ - master
+
+ - name: docker dashboard
+ image: plugins/docker
+ settings:
+ username:
+ from_secret: docker_username
+ password:
+ from_secret: docker_password
+ repo: nemunaire/fic-dashboard
+ auto_tag: true
+ auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
+ dockerfile: Dockerfile-dashboard
+ when:
+ branch:
+ - master
+
+ - name: docker qa
+ image: plugins/docker
+ settings:
+ username:
+ from_secret: docker_username
+ password:
+ from_secret: docker_password
+ repo: nemunaire/fic-qa
+ auto_tag: true
+ auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
+ dockerfile: Dockerfile-qa
+ when:
+ branch:
+ - master
+
+ - name: docker repochecker
+ image: plugins/docker
+ settings:
+ username:
+ from_secret: docker_username
+ password:
+ from_secret: docker_password
+ repo: nemunaire/fic-repochecker
+ auto_tag: true
+ auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
+ dockerfile: Dockerfile-repochecker
+ when:
+ branch:
+ - master
+
+trigger:
+ event:
+ - cron
+ - push
+ - tag
+
+---
+kind: pipeline
+name: docker-manifest
+steps:
+ - name: publish admin
+ image: plugins/manifest
+ settings:
+ auto_tag: true
+ ignore_missing: true
+ spec: .drone-manifest-fic-admin.yml
+ username:
+ from_secret: docker_username
+ password:
+ from_secret: docker_password
+
+ - name: publish checker
+ image: plugins/manifest
+ settings:
+ auto_tag: true
+ ignore_missing: true
+ spec: .drone-manifest-fic-checker.yml
+ username:
+ from_secret: docker_username
+ password:
+ from_secret: docker_password
+
+ - name: publish evdist
+ image: plugins/manifest
+ settings:
+ auto_tag: true
+ ignore_missing: true
+ spec: .drone-manifest-fic-evdist.yml
+ username:
+ from_secret: docker_username
+ password:
+ from_secret: docker_password
+
+ - name: publish generator
+ image: plugins/manifest
+ settings:
+ auto_tag: true
+ ignore_missing: true
+ spec: .drone-manifest-fic-generator.yml
+ username:
+ from_secret: docker_username
+ password:
+ from_secret: docker_password
+
+ - name: publish receiver
+ image: plugins/manifest
+ settings:
+ auto_tag: true
+ ignore_missing: true
+ spec: .drone-manifest-fic-receiver.yml
+ username:
+ from_secret: docker_username
+ password:
+ from_secret: docker_password
+
+ - name: publish frontend nginx
+ image: plugins/manifest
+ settings:
+ auto_tag: true
+ ignore_missing: true
+ spec: .drone-manifest-fic-nginx.yml
+ username:
+ from_secret: docker_username
+ password:
+ from_secret: docker_password
+
+ - name: publish frontend ui
+ image: plugins/manifest
+ settings:
+ auto_tag: true
+ ignore_missing: true
+ spec: .drone-manifest-fic-frontend-ui.yml
+ username:
+ from_secret: docker_username
+ password:
+ from_secret: docker_password
+
+ - name: publish dashboard
+ image: plugins/manifest
+ settings:
+ auto_tag: true
+ ignore_missing: true
+ spec: .drone-manifest-fic-dashboard.yml
+ username:
+ from_secret: docker_username
+ password:
+ from_secret: docker_password
+
+ - name: publish repochecker
+ image: plugins/manifest
+ settings:
+ auto_tag: true
+ ignore_missing: true
+ spec: .drone-manifest-fic-repochecker.yml
+ username:
+ from_secret: docker_username
+ password:
+ from_secret: docker_password
+
+ - name: publish qa
+ image: plugins/manifest
+ settings:
+ auto_tag: true
+ ignore_missing: true
+ spec: .drone-manifest-fic-qa.yml
+ username:
+ from_secret: docker_username
+ password:
+ from_secret: docker_password
+
+ - name: docker fic-get-remote-files
+ failure: ignore
+ image: plugins/docker
+ settings:
+ username:
+ from_secret: docker_username
+ password:
+ from_secret: docker_password
+ repo: nemunaire/fic-get-remote-files
+ auto_tag: true
+ auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
+ dockerfile: Dockerfile-get-remote-files
+ when:
+ branch:
+ - master
+
+ - name: publish fickit-deploy
+ image: plugins/manifest
+ settings:
+ auto_tag: true
+ ignore_missing: true
+ spec: .drone-manifest-fickit-deploy.yml
+ username:
+ from_secret: docker_username
+ password:
+ from_secret: docker_password
+
+trigger:
+ event:
+ - push
+ - tag
+
+depends_on:
+- build-amd64
+- build-arm64
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..4327ccd0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,43 @@
+vendor/
+DASHBOARD/
+FILES/
+PKI/
+REMOTE/
+SETTINGS/
+SETTINGSDIST/
+TEAMS/
+submissions/
+admin/sync/README.html
+fickit-boot-cmdline
+fickit-boot-initrd.img
+fickit-boot-kernel
+fickit-backend-cmdline
+fickit-backend-initrd.img
+fickit-backend-squashfs.img
+fickit-backend-kernel
+fickit-backend-state
+fickit-frontend-cmdline
+fickit-frontend-initrd.img
+fickit-frontend-squashfs.img
+fickit-frontend-kernel
+fickit-frontend-state
+fickit-prepare-bios.img
+fickit-prepare-cmdline
+fickit-prepare-initrd.img
+fickit-prepare-kernel
+fickit-prepare-state
+fickit-update-cmdline
+fickit-update-initrd.img
+fickit-update-kernel
+fickit-update-squashfs.img
+result
+started
+
+# Standalone binaries
+admin/get-remote-files/get-remote-files
+fic-admin
+fic-backend
+fic-dashboard
+fic-frontend
+fic-qa
+fic-repochecker
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 00000000..0e57016f
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,122 @@
+---
+
+stages:
+ - deps
+ - build
+ - fickit
+ - sast
+ - qa
+ - image
+ - container_scanning
+
+cache:
+ paths:
+ - .go/pkg/mod/
+ - qa/ui/node_modules/
+ - frontend/ui/node_modules/
+
+include:
+ - '.gitlab-ci/build.yml'
+ - '.gitlab-ci/image.yml'
+ - template: SAST.gitlab-ci.yml
+ - template: Security/Dependency-Scanning.gitlab-ci.yml
+ - template: Security/Secret-Detection.gitlab-ci.yml
+ - template: Security/Container-Scanning.gitlab-ci.yml
+
+.scanners-matrix:
+ parallel:
+ matrix:
+ - IMAGE_NAME: [checker, admin, evdist, frontend-ui, nginx, dashboard, repochecker, qa, receiver, generator, remote-challenge-sync-airbus]
+
+container_scanning:
+ stage: container_scanning
+ extends:
+ - .scanners-matrix
+ variables:
+ DOCKER_SERVICE: localhost
+ DOCKERFILE_PATH: Dockerfile-${IMAGE_NAME}
+ CI_APPLICATION_REPOSITORY: ${CI_REGISTRY_IMAGE}/${CI_COMMIT_REF_SLUG}/${IMAGE_NAME}
+ CI_APPLICATION_TAG: latest
+ GIT_STRATEGY: fetch
+ before_script:
+ - 'echo "Scanning: ${IMAGE_NAME}"'
+ rules:
+ - if: '$CI_COMMIT_BRANCH == "master"'
+
+sast:
+ stage: sast
+ interruptible: true
+ needs: []
+ before_script:
+ - rm -rf .go/
+
+secret_detection:
+ stage: sast
+ interruptible: true
+ needs: []
+
+dependency_scanning:
+ stage: qa
+ interruptible: true
+ needs: []
+
+get-deps:
+ stage: deps
+ image: golang:1-alpine
+ before_script:
+ - export GOPATH="$CI_PROJECT_DIR/.go"
+ - mkdir -p .go
+ script:
+ - apk --no-cache add git
+ - go get -v -d ./...
+
+vet:
+ stage: sast
+ needs: ["build-qa-ui"]
+ dependencies:
+ - build-qa-ui
+ image: golang:1-alpine
+ before_script:
+ - export GOPATH="$CI_PROJECT_DIR/.go"
+ - mkdir -p .go
+ script:
+ - apk --no-cache add build-base
+ - go vet -v -buildvcs=false -tags gitgo ./...
+ - go vet -v -buildvcs=false ./...
+
+fickit:
+ stage: fickit
+ interruptible: true
+ needs: ["build-admin","build-checker","build-dashboard","build-evdist","build-generator","build-qa","build-receiver","build-repochecker"]
+ image: nemunaire/linuxkit
+ tags: ['docker']
+ before_script:
+ - mkdir -p ~/.docker
+ - echo "{\"auths\":{\"${CI_REGISTRY}\":{\"username\":\"${CI_REGISTRY_USER}\",\"password\":\"${CI_REGISTRY_PASSWORD}\"}}}" > ~/.docker/config.json
+ script:
+ - dockerd & sleep 5
+
+ - linuxkit pkg push -force -org "${CI_REGISTRY_IMAGE}/${CI_COMMIT_REF_SLUG}" fickit-pkg/boot/
+ - linuxkit pkg push -force -org "${CI_REGISTRY_IMAGE}/${CI_COMMIT_REF_SLUG}" fickit-pkg/kexec/
+ - linuxkit pkg push -force -org "${CI_REGISTRY_IMAGE}/${CI_COMMIT_REF_SLUG}" fickit-pkg/mariadb-client/
+ - linuxkit pkg push -force -org "${CI_REGISTRY_IMAGE}/${CI_COMMIT_REF_SLUG}" fickit-pkg/mdadm/
+ - linuxkit pkg push -force -org "${CI_REGISTRY_IMAGE}/${CI_COMMIT_REF_SLUG}" fickit-pkg/rsync/
+ - linuxkit pkg push -force -org "${CI_REGISTRY_IMAGE}/${CI_COMMIT_REF_SLUG}" fickit-pkg/syslinux/
+ - linuxkit pkg push -force -org "${CI_REGISTRY_IMAGE}/${CI_COMMIT_REF_SLUG}" fickit-pkg/unbound/
+
+ - sed -i "s@nemunaire/fic-@${CI_REGISTRY_IMAGE}/master/@;s@nemunaire/@${CI_REGISTRY_IMAGE}/${CI_COMMIT_REF_SLUG}/@" fickit-backend.yml fickit-boot.yml fickit-frontend.yml fickit-prepare.yml fickit-update.yml
+
+ - linuxkit build -format kernel+squashfs fickit-backend.yml
+ - linuxkit build -format kernel+squashfs fickit-frontend.yml
+ - linuxkit build -format kernel+initrd fickit-boot.yml
+ - linuxkit build -format kernel+initrd fickit-prepare.yml
+ - linuxkit build -format kernel+initrd fickit-update.yml
+ artifacts:
+ expire_in: 8 hours
+ paths:
+ - fickit-backend-squashfs.img
+ - fickit-frontend-squashfs.img
+ - fickit-boot-kernel
+ - fickit-boot-initrd.img
+ - fickit-prepare-initrd.img
+ - fickit-update-initrd.img
diff --git a/.gitlab-ci/build.yml b/.gitlab-ci/build.yml
new file mode 100644
index 00000000..621d8bbb
--- /dev/null
+++ b/.gitlab-ci/build.yml
@@ -0,0 +1,93 @@
+---
+
+.build:
+ stage: build
+ image: golang:1-alpine
+ before_script:
+ - export GOPATH="$CI_PROJECT_DIR/.go"
+ - mkdir -p .go
+ variables:
+ CGO_ENABLED: 0
+
+build-qa-ui:
+ stage: build
+ image: node:21-alpine
+ before_script:
+ script:
+ - cd qa/ui
+ - npm install --network-timeout=100000
+ - npm run build
+ artifacts:
+ paths:
+ - qa/ui/build/
+ when: on_success
+
+build-checker:
+ extends:
+ - .build
+ script:
+ - go build -v -buildvcs=false -o deploy/checker srs.epita.fr/fic-server/checker
+
+build-generator:
+ extends:
+ - .build
+ script:
+ - go build -v -buildvcs=false -o deploy/generator srs.epita.fr/fic-server/generator
+
+build-receiver:
+ extends:
+ - .build
+ script:
+ - go build -v -buildvcs=false -o deploy/receiver srs.epita.fr/fic-server/receiver
+
+build-admin:
+ extends:
+ - .build
+ script:
+ - go build -v -buildvcs=false -tags gitgo -o deploy/admin-gitgo srs.epita.fr/fic-server/admin
+ - go build -v -buildvcs=false -o deploy/admin srs.epita.fr/fic-server/admin
+
+build-evdist:
+ extends:
+ - .build
+ script:
+ - go build -v -buildvcs=false -o deploy/evdist srs.epita.fr/fic-server/evdist
+
+build-frontend-ui:
+ stage: build
+ image: node:21-alpine
+ before_script:
+ script:
+ - cd frontend/fic
+ - npm install --network-timeout=100000
+ - npm run build
+
+build-dashboard:
+ extends:
+ - .build
+ script:
+ - go build -v -buildvcs=false -o deploy/dashboard srs.epita.fr/fic-server/dashboard
+
+build-repochecker:
+ extends:
+ - .build
+ variables:
+ CGO_ENABLED: 1
+ script:
+ - apk --no-cache add build-base
+ - go build -buildvcs=false --tags checkupdate -v -o deploy/repochecker srs.epita.fr/fic-server/repochecker
+ - go build -buildvcs=false -buildmode=plugin -v -o deploy/repochecker-epita-rules.so srs.epita.fr/fic-server/repochecker/epita
+ - go build -buildvcs=false -buildmode=plugin -v -o deploy/repochecker-file-inspector-rules.so srs.epita.fr/fic-server/repochecker/file-inspector
+ - go build -buildvcs=false -buildmode=plugin -v -o deploy/repochecker-grammalecte-rules.so srs.epita.fr/fic-server/repochecker/grammalecte
+ - go build -buildvcs=false -buildmode=plugin -v -o deploy/repochecker-pcap-inspector-rules.so srs.epita.fr/fic-server/repochecker/pcap-inspector
+ - go build -buildvcs=false -buildmode=plugin -v -o deploy/repochecker-videos-rules.so srs.epita.fr/fic-server/repochecker/videos
+ - grep "const version" repochecker/update.go | sed -r 's/^.*=\s*(\S.*)$/\1/' > deploy/repochecker.version
+
+build-qa:
+ extends:
+ - .build
+ needs: ["build-qa-ui"]
+ dependencies:
+ - build-qa-ui
+ script:
+ - go build -v -buildvcs=false -o deploy/qa srs.epita.fr/fic-server/qa
diff --git a/.gitlab-ci/image.yml b/.gitlab-ci/image.yml
new file mode 100644
index 00000000..988bb2b0
--- /dev/null
+++ b/.gitlab-ci/image.yml
@@ -0,0 +1,99 @@
+---
+
+.push:
+ stage: image
+ interruptible: true
+ needs: []
+ image:
+ name: gcr.io/kaniko-project/executor:v1.9.0-debug
+ entrypoint: [""]
+ before_script:
+ - mkdir -p /kaniko/.docker
+ - echo "{\"auths\":{\"${CI_REGISTRY}\":{\"username\":\"${CI_REGISTRY_USER}\",\"password\":\"${CI_REGISTRY_PASSWORD}\"}}}" > /kaniko/.docker/config.json
+ script:
+ - |
+ /kaniko/executor \
+ --context . \
+ --dockerfile "${DOCKERFILE}" \
+ --destination "${CI_REGISTRY_IMAGE}/${CI_COMMIT_REF_SLUG}/${CI_JOB_NAME}:${CI_COMMIT_SHA}" \
+ --destination "${CI_REGISTRY_IMAGE}/${CI_COMMIT_REF_SLUG}/${CI_JOB_NAME}:latest"
+ only:
+ - master
+
+checker:
+ extends:
+ - .push
+ variables:
+ DOCKERFILE: Dockerfile-checker
+
+receiver:
+ extends:
+ - .push
+ variables:
+ DOCKERFILE: Dockerfile-receiver
+
+generator:
+ extends:
+ - .push
+ variables:
+ DOCKERFILE: Dockerfile-generator
+
+admin:
+ extends:
+ - .push
+ variables:
+ DOCKERFILE: Dockerfile-admin
+
+fickit-deploy:
+ extends:
+ - .push
+ variables:
+ DOCKERFILE: Dockerfile-deploy
+
+get-remote-files:
+ extends:
+ - .push
+ variables:
+ DOCKERFILE: Dockerfile-get-remote-files
+
+evdist:
+ extends:
+ - .push
+ variables:
+ DOCKERFILE: Dockerfile-evdist
+
+frontend-ui:
+ extends:
+ - .push
+ variables:
+ DOCKERFILE: Dockerfile-frontend-ui
+
+nginx:
+ extends:
+ - .push
+ variables:
+ DOCKERFILE: Dockerfile-nginx
+
+dashboard:
+ extends:
+ - .push
+ variables:
+ DOCKERFILE: Dockerfile-dashboard
+
+repochecker:
+ extends:
+ - .push
+ variables:
+ DOCKERFILE: Dockerfile-repochecker
+
+qa:
+ extends:
+ - .push
+ variables:
+ DOCKERFILE: Dockerfile-qa
+
+remote-challenge-sync-airbus:
+ extends:
+ - .push
+ variables:
+ DOCKERFILE: Dockerfile-remote-challenge-sync-airbus
diff --git a/Dockerfile-admin b/Dockerfile-admin
new file mode 100644
index 00000000..f2c285f2
--- /dev/null
+++ b/Dockerfile-admin
@@ -0,0 +1,42 @@
+FROM golang:1-alpine AS gobuild
+
+RUN apk add --no-cache git
+
+WORKDIR /go/src/srs.epita.fr/fic-server/
+
+RUN apk add --no-cache binutils-gold build-base
+
+COPY go.mod go.sum ./
+COPY settings settings/
+COPY libfic ./libfic/
+COPY admin ./admin/
+COPY repochecker ./repochecker/
+
+RUN go get -d -v ./admin && \
+ go build -v -o admin/admin ./admin && \
+ go build -v -buildmode=plugin -o repochecker/epita-rules.so ./repochecker/epita && \
+ go build -v -buildmode=plugin -o repochecker/file-inspector.so ./repochecker/file-inspector && \
+ go build -v -buildmode=plugin -o repochecker/grammalecte-rules.so ./repochecker/grammalecte && \
+ go build -v -buildmode=plugin -o repochecker/videos-rules.so ./repochecker/videos
+
+
+FROM alpine:3.21
+
+RUN apk add --no-cache \
+ ca-certificates \
+ git \
+ git-lfs \
+ openssh-client-default \
+ openssl
+
+EXPOSE 8081
+
+WORKDIR /srv
+
+ENTRYPOINT ["/srv/admin", "-bind=:8081", "-baseurl=/admin/"]
+
+COPY --from=gobuild /go/src/srs.epita.fr/fic-server/admin/admin /srv/admin
+COPY --from=gobuild /go/src/srs.epita.fr/fic-server/repochecker/epita-rules.so /srv/epita-rules.so
+COPY --from=gobuild /go/src/srs.epita.fr/fic-server/repochecker/file-inspector.so /usr/lib/file-inspector.so
+COPY --from=gobuild /go/src/srs.epita.fr/fic-server/repochecker/grammalecte-rules.so /usr/lib/grammalecte-rules.so
+COPY --from=gobuild /go/src/srs.epita.fr/fic-server/repochecker/videos-rules.so /usr/lib/videos-rules.so
diff --git a/Dockerfile-checker b/Dockerfile-checker
new file mode 100644
index 00000000..144ac0a7
--- /dev/null
+++ b/Dockerfile-checker
@@ -0,0 +1,22 @@
+FROM golang:1-alpine AS gobuild
+
+RUN apk add --no-cache git
+
+WORKDIR /go/src/srs.epita.fr/fic-server/
+
+COPY go.mod go.sum ./
+COPY settings settings/
+COPY libfic ./libfic/
+COPY checker ./checker/
+
+RUN go get -d -v ./checker && \
+ go build -v -buildvcs=false -o checker/checker ./checker
+
+
+FROM alpine:3.21
+
+WORKDIR /srv
+
+ENTRYPOINT ["/srv/checker"]
+
+COPY --from=gobuild /go/src/srs.epita.fr/fic-server/checker/checker /srv/checker
diff --git a/Dockerfile-dashboard b/Dockerfile-dashboard
new file mode 100644
index 00000000..b8291bba
--- /dev/null
+++ b/Dockerfile-dashboard
@@ -0,0 +1,32 @@
+FROM golang:1-alpine AS gobuild
+
+RUN apk add --no-cache git
+
+WORKDIR /go/src/srs.epita.fr/fic-server/
+
+COPY go.mod go.sum ./
+COPY settings settings/
+COPY libfic ./libfic/
+COPY dashboard ./dashboard/
+
+RUN go get -d -v ./dashboard && \
+ go build -v -buildvcs=false -o dashboard/dashboard ./dashboard
+
+
+FROM alpine:3.21
+
+EXPOSE 8082
+
+WORKDIR /srv
+
+ENTRYPOINT ["/srv/dashboard", "--bind=:8082"]
+
+VOLUME /srv/htdocs-dashboard/
+
+COPY --from=gobuild /go/src/srs.epita.fr/fic-server/dashboard/dashboard /srv/dashboard
+COPY dashboard/static/index.html /srv/htdocs-dashboard/
+COPY admin/static/css/bootstrap.min.css dashboard/static/css/fic.css admin/static/css/glyphicon.css /srv/htdocs-dashboard/css/
+COPY admin/static/fonts /srv/htdocs-dashboard/fonts
+COPY dashboard/static/img/srs.png /srv/htdocs-dashboard/img/
+COPY dashboard/static/js/dashboard.js admin/static/js/angular.min.js dashboard/static/js/angular-animate.min.js admin/static/js/angular-route.min.js admin/static/js/angular-sanitize.min.js admin/static/js/bootstrap.min.js admin/static/js/common.js admin/static/js/d3.v3.min.js admin/static/js/jquery.min.js /srv/htdocs-dashboard/js/
+COPY admin/static/js/i18n/* /srv/htdocs-dashboard/js/i18n/
diff --git a/Dockerfile-deploy b/Dockerfile-deploy
new file mode 100644
index 00000000..99765543
--- /dev/null
+++ b/Dockerfile-deploy
@@ -0,0 +1,24 @@
+FROM alpine:3.21
+
+EXPOSE 67/udp
+EXPOSE 69/udp
+EXPOSE 80/tcp
+
+ENTRYPOINT ["/usr/sbin/initial-config.sh"]
+CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]
+
+WORKDIR /srv/s
+
+RUN apk add --no-cache \
+ busybox-extras \
+ supervisor \
+ syslinux \
+ tftp-hpa
+
+RUN touch /var/lib/udhcpd/udhcpd.leases && \
+ mv /usr/share/syslinux/* /srv
+
+COPY configs/deploy-initial-config.sh /usr/sbin/initial-config.sh
+COPY configs/deploy-supervisord.conf /etc/supervisord.conf
+COPY configs/udhcpd-sample.conf /etc/udhcpd.conf
+COPY configs/pxelinux.cfg /srv/pxelinux.cfg/default
\ No newline at end of file
diff --git a/Dockerfile-evdist b/Dockerfile-evdist
new file mode 100644
index 00000000..45a2c506
--- /dev/null
+++ b/Dockerfile-evdist
@@ -0,0 +1,21 @@
+FROM golang:1-alpine AS gobuild
+
+RUN apk add --no-cache git
+
+WORKDIR /go/src/srs.epita.fr/fic-server/
+
+COPY go.mod go.sum ./
+COPY settings settings/
+COPY evdist ./evdist/
+
+RUN go get -d -v ./evdist && \
+ go build -v -buildvcs=false -o evdist/evdist ./evdist
+
+
+FROM alpine:3.21
+
+WORKDIR /srv
+
+ENTRYPOINT ["/srv/evdist"]
+
+COPY --from=gobuild /go/src/srs.epita.fr/fic-server/evdist/evdist /srv/evdist
diff --git a/Dockerfile-frontend-ui b/Dockerfile-frontend-ui
new file mode 100644
index 00000000..a14b0c58
--- /dev/null
+++ b/Dockerfile-frontend-ui
@@ -0,0 +1,13 @@
+FROM node:23-alpine AS nodebuild
+
+WORKDIR /ui
+
+COPY frontend/fic/ .
+
+RUN npm install --network-timeout=100000 && \
+ npm run build
+
+
+FROM scratch
+
+COPY --from=nodebuild /ui/build/ /www/htdocs-frontend
diff --git a/Dockerfile-generator b/Dockerfile-generator
new file mode 100644
index 00000000..2574e614
--- /dev/null
+++ b/Dockerfile-generator
@@ -0,0 +1,22 @@
+FROM golang:1-alpine AS gobuild
+
+RUN apk add --no-cache git
+
+WORKDIR /go/src/srs.epita.fr/fic-server/
+
+COPY go.mod go.sum ./
+COPY settings settings/
+COPY libfic ./libfic/
+COPY generator ./generator/
+
+RUN go get -d -v ./generator && \
+ go build -v -buildvcs=false -o generator/generator ./generator
+
+
+FROM alpine:3.21
+
+WORKDIR /srv
+
+ENTRYPOINT ["/srv/generator"]
+
+COPY --from=gobuild /go/src/srs.epita.fr/fic-server/generator/generator /srv/generator
diff --git a/Dockerfile-get-remote-files b/Dockerfile-get-remote-files
new file mode 100644
index 00000000..95e1c5f3
--- /dev/null
+++ b/Dockerfile-get-remote-files
@@ -0,0 +1,27 @@
+FROM golang:1-alpine AS gobuild
+
+RUN apk add --no-cache git
+
+WORKDIR /go/src/srs.epita.fr/fic-server/
+
+RUN apk add --no-cache build-base
+
+COPY go.mod go.sum ./
+COPY settings settings/
+COPY libfic ./libfic/
+COPY admin ./admin/
+
+RUN go get -d -v ./admin && \
+ go build -v -o get-remote-files ./admin/get-remote-files
+
+
+FROM alpine:3.21
+
+RUN apk add --no-cache \
+ ca-certificates
+
+WORKDIR /srv
+
+ENTRYPOINT ["/srv/get-remote-files", "/mnt/fic/"]
+
+COPY --from=gobuild /go/src/srs.epita.fr/fic-server/get-remote-files /srv/get-remote-files
diff --git a/Dockerfile-nginx b/Dockerfile-nginx
new file mode 100644
index 00000000..41326840
--- /dev/null
+++ b/Dockerfile-nginx
@@ -0,0 +1,32 @@
+FROM node:23-alpine AS nodebuild
+
+WORKDIR /ui
+
+COPY frontend/fic/ .
+
+RUN npm install --network-timeout=100000 && \
+ npm run build
+
+
+FROM nginx:stable-alpine-slim
+
+ENV FIC_BASEURL /
+ENV HOST_RECEIVER receiver:8080
+ENV HOST_ADMIN admin:8081
+ENV HOST_DASHBOARD dashboard:8082
+ENV HOST_QA qa:8083
+ENV PATH_FILES /srv/FILES
+ENV PATH_STARTINGBLOCK /srv/STARTINGBLOCK
+ENV PATH_STATIC /srv/htdocs-frontend
+ENV PATH_SETTINGS /srv/SETTINGSDIST
+ENV PATH_TEAMS /srv/TEAMS
+
+EXPOSE 80
+
+COPY configs/nginx-chbase.sh /docker-entrypoint.d/40-update-baseurl.sh
+
+COPY configs/nginx/get-team/upstream.conf /etc/nginx/fic-get-team.conf
+COPY configs/nginx/auth/none.conf /etc/nginx/fic-auth.conf
+COPY configs/nginx/base/docker.conf /etc/nginx/templates/default.conf.template
+
+COPY --from=nodebuild /ui/build/ /srv/htdocs-frontend
diff --git a/Dockerfile-qa b/Dockerfile-qa
new file mode 100644
index 00000000..37f3a1b0
--- /dev/null
+++ b/Dockerfile-qa
@@ -0,0 +1,38 @@
+FROM node:23-alpine AS nodebuild
+
+WORKDIR /ui
+
+COPY qa/ui/ .
+
+RUN npm install --network-timeout=100000 && \
+ npm run build
+
+
+FROM golang:1-alpine AS gobuild
+
+RUN apk add --no-cache git
+
+WORKDIR /go/src/srs.epita.fr/fic-server/
+
+COPY go.mod go.sum ./
+COPY settings settings/
+COPY libfic ./libfic/
+COPY --from=nodebuild /ui ./qa/ui
+COPY qa ./qa/
+COPY admin ./admin/
+
+RUN go get -d -v ./qa && \
+ go build -v -buildvcs=false -o qa/qa ./qa
+
+
+FROM alpine:3.21
+
+EXPOSE 8083
+
+WORKDIR /srv
+
+ENTRYPOINT ["/srv/qa", "--bind=:8083"]
+
+VOLUME /srv/htdocs-qa/
+
+COPY --from=gobuild /go/src/srs.epita.fr/fic-server/qa/qa /srv/qa
diff --git a/Dockerfile-receiver b/Dockerfile-receiver
new file mode 100644
index 00000000..f2cac038
--- /dev/null
+++ b/Dockerfile-receiver
@@ -0,0 +1,27 @@
+FROM golang:1-alpine AS gobuild
+
+RUN apk add --no-cache git
+
+WORKDIR /go/src/srs.epita.fr/fic-server/
+
+COPY go.mod go.sum ./
+COPY settings settings/
+COPY libfic ./libfic/
+COPY receiver ./receiver/
+
+RUN go get -d -v ./receiver && \
+ go build -v -buildvcs=false -o ./receiver/receiver ./receiver
+
+
+FROM alpine:3.21
+
+EXPOSE 8080
+
+WORKDIR /srv
+
+ENTRYPOINT ["/usr/sbin/entrypoint.sh"]
+CMD ["--bind=:8080"]
+
+COPY entrypoint-receiver.sh /usr/sbin/entrypoint.sh
+
+COPY --from=gobuild /go/src/srs.epita.fr/fic-server/receiver/receiver /srv/receiver
diff --git a/Dockerfile-remote-challenge-sync-airbus b/Dockerfile-remote-challenge-sync-airbus
new file mode 100644
index 00000000..47a5e167
--- /dev/null
+++ b/Dockerfile-remote-challenge-sync-airbus
@@ -0,0 +1,24 @@
+FROM golang:1-alpine AS gobuild
+
+RUN apk add --no-cache git
+
+WORKDIR /go/src/srs.epita.fr/fic-server/
+
+COPY go.mod go.sum ./
+COPY libfic ./libfic/
+COPY settings ./settings/
+COPY remote/challenge-sync-airbus ./remote/challenge-sync-airbus/
+
+RUN go get -d -v ./remote/challenge-sync-airbus && \
+ go build -v -buildvcs=false -o ./challenge-sync-airbus ./remote/challenge-sync-airbus
+
+
+FROM alpine:3.21
+
+RUN apk add --no-cache openssl ca-certificates
+
+WORKDIR /srv
+
+ENTRYPOINT ["/srv/challenge-sync-airbus"]
+
+COPY --from=gobuild /go/src/srs.epita.fr/fic-server/challenge-sync-airbus /srv/challenge-sync-airbus
diff --git a/Dockerfile-remote-scores-sync-zqds b/Dockerfile-remote-scores-sync-zqds
new file mode 100644
index 00000000..e5ff87fb
--- /dev/null
+++ b/Dockerfile-remote-scores-sync-zqds
@@ -0,0 +1,24 @@
+FROM golang:1-alpine AS gobuild
+
+RUN apk add --no-cache git
+
+WORKDIR /go/src/srs.epita.fr/fic-server/
+
+COPY go.mod go.sum ./
+COPY libfic ./libfic/
+COPY settings ./settings/
+COPY remote/scores-sync-zqds ./remote/scores-sync-zqds/
+
+RUN go get -d -v ./remote/scores-sync-zqds && \
+ go build -v -buildvcs=false -o ./scores-sync-zqds ./remote/scores-sync-zqds
+
+
+FROM alpine:3.21
+
+RUN apk add --no-cache openssl ca-certificates
+
+WORKDIR /srv
+
+ENTRYPOINT ["/srv/scores-sync-zqds"]
+
+COPY --from=gobuild /go/src/srs.epita.fr/fic-server/scores-sync-zqds /srv/scores-sync-zqds
diff --git a/Dockerfile-repochecker b/Dockerfile-repochecker
new file mode 100644
index 00000000..d02bc1de
--- /dev/null
+++ b/Dockerfile-repochecker
@@ -0,0 +1,42 @@
+FROM golang:1-alpine AS gobuild
+
+RUN apk add --no-cache git
+
+WORKDIR /go/src/srs.epita.fr/fic-server/
+
+RUN apk add --no-cache binutils-gold build-base
+
+COPY go.mod go.sum ./
+COPY settings settings/
+COPY libfic ./libfic/
+COPY admin ./admin/
+COPY repochecker ./repochecker/
+
+RUN go get -d -v ./repochecker && \
+ go build -v -o repochecker/repochecker ./repochecker && \
+ go build -v -buildmode=plugin -o repochecker/epita-rules.so ./repochecker/epita && \
+ go build -v -buildmode=plugin -o repochecker/file-inspector.so ./repochecker/file-inspector && \
+ go build -v -buildmode=plugin -o repochecker/grammalecte-rules.so ./repochecker/grammalecte && \
+ go build -v -buildmode=plugin -o repochecker/pcap-inspector.so ./repochecker/pcap-inspector && \
+ go build -v -buildmode=plugin -o repochecker/videos-rules.so ./repochecker/videos
+
+
+ENV GRAMMALECTE_VERSION 2.1.1
+
+ADD https://web.archive.org/web/20240926154729if_/https://grammalecte.net/zip/Grammalecte-fr-v$GRAMMALECTE_VERSION.zip /srv/grammalecte.zip
+
+RUN mkdir /srv/grammalecte && cd /srv/grammalecte && unzip /srv/grammalecte.zip && sed -i 's/if sys.version_info.major < (3, 7):/if False:/' /srv/grammalecte/grammalecte-server.py
+
+FROM alpine:3.19
+
+ENTRYPOINT ["/usr/bin/repochecker", "--rules-plugins=/usr/lib/epita-rules.so", "--rules-plugins=/usr/lib/file-inspector.so", "--rules-plugins=/usr/lib/grammalecte-rules.so", "--rules-plugins=/usr/lib/pcap-inspector.so", "--rules-plugins=/usr/lib/videos-rules.so"]
+
+RUN apk add --no-cache git python3 ffmpeg
+
+COPY --from=gobuild /srv/grammalecte /srv/grammalecte
+COPY --from=gobuild /go/src/srs.epita.fr/fic-server/repochecker/repochecker /usr/bin/repochecker
+COPY --from=gobuild /go/src/srs.epita.fr/fic-server/repochecker/epita-rules.so /usr/lib/epita-rules.so
+COPY --from=gobuild /go/src/srs.epita.fr/fic-server/repochecker/file-inspector.so /usr/lib/file-inspector.so
+COPY --from=gobuild /go/src/srs.epita.fr/fic-server/repochecker/grammalecte-rules.so /usr/lib/grammalecte-rules.so
+COPY --from=gobuild /go/src/srs.epita.fr/fic-server/repochecker/pcap-inspector.so /usr/lib/pcap-inspector.so
+COPY --from=gobuild /go/src/srs.epita.fr/fic-server/repochecker/videos-rules.so /usr/lib/videos-rules.so
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..5b7eaadc
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2016-2018 Pierre-Olivier Mercier
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..d908ce27
--- /dev/null
+++ b/README.md
@@ -0,0 +1,238 @@
+FIC Forensic CTF Platform
+=========================
+
+This is a CTF server for distributing and validating challenges. It is design
+to be robust, so it uses some uncommon technics like client certificate for
+authentication, lots of state of the art cryptographic methods and aims to be
+deployed in a DMZ network architecture.
+
+## Features
+
+- **Collaborative Challenge Design and Review:** Facilitates large team collaboration for challenge creation and review.
+- **Versatile Flag Formats:** Supports flags as strings, numbers, multiple-choice questions, unique-choice questions, selects, multiline inputs, and strings with capture regexp.
+- **Engaging Challenge Interface:** A visually appealing interface that incorporates images to illustrate exercises.
+- **Public Dashboard:** Allow spectators to follow the competition alongside players.
+- **Archival Mode:** Preserve past challenges and data in a static form, with no code. Your archive can lied on a S3 bucket.
+- **Export Capabilities:** Export challenges to other CTF platforms.
+- **Security-Focused:** Designed with security as a top priority. Each service aims to be isolated with right restrictions. Answers are not stored in the database, ...
+- **Choose your Authentication:** Authentication is not part of this project, integrate your own authentication methods.
+- **Extensible:** Easily extend and customize the platform. The main codebase in Golang is highly documented, each frontend part can be recreated in another language with ease.
+- **Comprehensive Settings:** A wide range of settings for challenge customization. You can have first blood or not, dynamic exercice gain, evenemential bonus, ...
+- **Git Integration:** Seamless verification and integration with Git.
+- **Infrastructure as Code (IaC):** Ensure read-only and reproducible infrastructure.
+- **Last-Minute Checks:** Ensure your challenge is ready with a comprehensive set of checks that can be performed anytime, verifying that downloadable files are as expected by the challenge creators.
+- **Lightweight:** Optimized for minimal resource consumption, supporting features like serving gzipped files directly to browsers without CPU usage.
+- **Scalable:** Designed to handle large-scale competitions with multiple receivers and frontend servers, smoothly queuing activity peaks on the backend.
+- **Offline Capability:** Run your challenges offline.
+- **Integrated Exercise Issue Ticketing System:** Manage and track issues related to exercises during the competition directly with teams. During designing phase, this transform in a complete dedicated QA platform.
+- **Detailed Statistics:** Provide administrators with insights into exercise preferences and complexity.
+- **Change Planning:** Schedule events in advance, such as new exercise availability or ephemeral bonuses, with second-by-second precision.
+- **Frontend Time Synchronization:** Ensure accurate remaining time and event synchronization between servers and players.
+
+
+## Overview
+
+This is a [monorepo](https://danluu.com/monorepo/), containing several
+micro-services :
+
+- `admin` is the web interface and API used to control the challenge
+ and doing synchronization.
+- `checker` is an inotify reacting service that handles submissions
+ checking.
+- `dashboard` is a public interface to explain and follow the
+ conquest, aims to animate the challenge for visitors.
+- `evdist` is an inotify reacting service that handles settings
+ changes during the challenge (eg. a 30 minutes event where hints are
+ free, ...).
+- `generator` takes care of global and team's files generation.
+- `qa` is an interface dedicated to challenge development, it stores
+ reports to be treated by challenges creators.
+- `receiver` is only responsible for receiving submissions. It is the
+ only dynamic part accessibe to players, so it's codebase is reduce
+ to the minimum. It does not parse or try to understand players
+ submissions, it just write it down to a file in the file
+ system. Parsing and treatment is made by the `checker`.
+- `remote/challenge-sync-airbus` is an inotify reacting service that
+ allows us to synchronize scores and exercice validations with the
+ Airbus scoring platform.
+- `remote/scores-sync-zqds` is an inotify reacting service that allows
+ us to synchronize scores with the ZQDS scoring platform.
+- `repochecker` is a side project to check offline for synchronization
+ issues.
+
+Here is how thoses services speak to each others:
+
+
+
+In the production setup, each micro-service runs in a dedicated
+container, isolated from each other. Moreover, two physical machines
+should be used:
+
+- `phobos` communicates with players: displaying the web interface,
+ authenticate teams and players, storing contest files and handling
+ submissions retrieval without understanding them. It can't access
+ `deimos` so its job stops after writing requests on the filesystem.
+- `deimos` is hidden from players, isolated from the network. It can
+ only access `phobos` via a restricted ssh connection, to retrieve
+ requests from `phobos` filesystem and pushing to it newly generated
+ static files.
+
+Concretely, the L2 looks like this:
+
+
+
+So, the general filesystem is organized this way:
+
+- `DASHBOARD` contains files structuring the content of the dashboard
+ screen(s).
+- `FILES` stores the contest file to be downloaded by players. To be
+ accessible without authentication and to avoid bruteforce, each file
+ is placed into a directory with a hashed name (the original file
+ name is preserved). It's rsynced as is to `deimos`.
+- `GENERATOR` contains a socket to allow other services to communicate
+ with the `generator`.
+- `PKI` takes care of the PKI used for the client certiciate
+ authorization process, and more generaly, all authentication related
+ files (htpasswd, dexidp config, ...). Only the `shared` subdirectory
+ is shared with `deimos`, private key and teams P12 don't go out.
+- `SETTINGS` stores the challenge config as wanted by admins. It's not
+ always the config in use: it uses can be delayed waiting for a
+ trigger.
+- `SETTINGSDIST` is the challenge configuration in use. It is the one
+ shared with players.
+- `startingblock` keep the `started` state of the challenge. This
+ helps `nginx` to know when it can start distributing exercices
+ related files.
+- `TEAMS` stores the static files generated by the `generator`, there is
+ one subdirectory by team (id of the team), plus some files at the
+ root, which are common to all teams. There is also symlink pointing
+ to team directory, each symlink represent an authentication
+ association (certificate ID, OpenID username, htpasswd user, ...).
+- `submissions` is the directory where the `receiver` writes
+ requests. It creates subdirectories at the name of the
+ authentication association, as seen in `TEAMS`, `checker` then
+ resolve the association regarding `TEAMS` directory. There is also a
+ special directory to handle team registration.
+
+Here is a diagram showing how each micro-service uses directories it has access to (blue for read access, red for write access):
+
+
+
+Local developer setup
+---------------------
+
+### Using Docker
+
+Use `docker-compose build`, then `docker-compose up` to launch the infrastructure.
+
+After booting, you'll be able to reach the main interface at:
+ and the admin one at: (or at ).
+The dashboard is available at and the QA service at .
+
+In this setup, there is no authentication. You are identfied [as a team](./configs/nginx/get-team/team-1.conf). On first use you'll need to register.
+
+#### Import folder
+
+##### Local import folder
+The following changes is only required if your are trying to change the local import folder `~/fic` location.
+
+Make the following changes inside this file `docker-compose.yml`:
+
+ 23 volumes:
+ 24 - - ~/fic:/mnt/fic:ro
+ 24 + - /fic:/mnt/fic:ro
+
+##### Git import
+A git repository can be used:
+
+ 29 - command: --baseurl /admin/ -localimport /mnt/fic -localimportsymlink
+ 29 + command: --baseurl /admin/ -localimport /mnt/fic -localimportsymlink -git-import-remote git@gitlab.cri.epita.fr:ing/majeures/srs/fic/2042/challenges.git
+
+##### Owncloud import folder
+If your are trying to use the folder available with the Owncloud service, make the following changes inside this file `docker-compose.yml`:
+
+ 29 - command: --baseurl /admin/ -localimport /mnt/fic -localimportsymlink
+ 29 + command: --baseurl /admin/ -clouddav=https://owncloud.srs.epita.fr/remote.php/webdav/FIC%202019/ -clouduser -cloudpass ''
+
+### Manual builds
+
+Running this project requires a web server (configuration is given for nginx),
+a database (currently supporting only MySQL/MariaDB), a Go compiler for the
+revision 1.18 at least and a `inotify`-aware system. You'll also need NodeJS to
+compile some user interfaces.
+
+1. Above all, you need to build Node projects:
+
+ cd frontend/fic; npm install && npm run build
+ cd qa/ui; npm install && npm run build
+
+2. First, you'll need to retrieve the dependencies:
+
+ go mod vendor
+
+2. Then, build the three Go projects:
+
+ go build -o fic-admin ./admin
+ go build -o fic-checker ./checker
+ go build -o fic-dashboard ./dashboard
+ go build -o fic-generator ./generator
+ go build -o fic-qa ./qa
+ go build -o fic-receiver ./receiver
+ go build -o fic-repochecker ./repochecker
+ ...
+
+3. Before launching anything, you need to create a database:
+
+ mysql -u root -p < : this is the administration part.
+
+ ./fic-generator &
+
+ This daemon generates static and team related files and then waits
+ another process to tell it to regenerate some files.
+
+ ./fic-receiver &
+
+ This one exposes an API that gives time synchronization to clients and
+ handle submission reception (but without treating them).
+
+ ./fic-checker &
+
+ This service waits for new submissions (expected in `submissions`
+ directory). It only watchs modifications on the file system, it has no web
+ interface.
+
+ ./fic-dashboard &
+
+ This last server runs the public dashboard. It serves all file, without the
+ need of a webserver. It listens on port 8082 by default.
+
+ ./fic-qa &
+
+ If you need it, this will launch a web interface on the port 8083 by
+ default, to perform quality control.
+
+For the moment, a web server is mandatory to serve static files, look
+at the samples given in the `configs/nginx` directory. You need to
+pick one base configation flavor in the `configs/nginx/base`
+directory, and associated with an authentication mechanism in
+`configs/nginx/auth` (named the file `fic-auth.conf` in `/etc/nginx`),
+and also pick the corresponding `configs/nginx/get-team` file, you
+named `fic-get-team.conf`.
diff --git a/admin/.gitignore b/admin/.gitignore
index e2c3cd5d..0ccbc66d 100644
--- a/admin/.gitignore
+++ b/admin/.gitignore
@@ -2,3 +2,4 @@ admin
fic.db
PKI/
FILES/
+static/full_import_report.json
diff --git a/admin/api/certificate.go b/admin/api/certificate.go
index eb9de979..5c98c116 100644
--- a/admin/api/certificate.go
+++ b/admin/api/certificate.go
@@ -1,31 +1,479 @@
package api
import (
+ "crypto/rand"
+ "crypto/sha1"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/base32"
+ "encoding/base64"
+ "errors"
+ "fmt"
"io/ioutil"
+ "log"
+ "math"
+ "math/big"
+ "net/http"
"os"
+ "path"
+ "strconv"
+ "strings"
+ "time"
+ "srs.epita.fr/fic-server/admin/pki"
"srs.epita.fr/fic-server/libfic"
+
+ "github.com/gin-gonic/gin"
)
-func init() {
- router.Path("/ca").Methods("GET").HandlerFunc(apiHandler(genCA))
+var TeamsDir string
- rt := router.PathPrefix("/teams/{tid}/certificate").Subrouter()
- rt.Path("/").Methods("GET").HandlerFunc(apiHandler(teamHandler(GetTeamCertificate)))
- rt.Path("/generate").Methods("GET").HandlerFunc(apiHandler(teamHandler(
- func(team fic.Team, args map[string]string, body []byte) (interface{}, error) { return team.GenerateCert(), nil })))
- rt.Path("/revoke").Methods("GET").HandlerFunc(apiHandler(teamHandler(
- func(team fic.Team, args map[string]string, body []byte) (interface{}, error) { return team.RevokeCert(), nil })))
+func declareCertificateRoutes(router *gin.RouterGroup) {
+ router.GET("/htpasswd", func(c *gin.Context) {
+ ret, err := genHtpasswd(true)
+ if err != nil {
+ c.AbortWithError(http.StatusInternalServerError, err)
+ return
+ }
+ c.String(http.StatusOK, ret)
+ })
+ router.POST("/htpasswd", func(c *gin.Context) {
+ if htpasswd, err := genHtpasswd(true); err != nil {
+ log.Println("Unable to generate htpasswd:", err)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ } else if err := ioutil.WriteFile(path.Join(pki.PKIDir, "shared", "ficpasswd"), []byte(htpasswd), 0644); err != nil {
+ log.Println("Unable to write htpasswd:", err)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+ c.AbortWithStatus(http.StatusOK)
+ })
+ router.DELETE("/htpasswd", func(c *gin.Context) {
+ if err := os.Remove(path.Join(pki.PKIDir, "shared", "ficpasswd")); err != nil {
+ log.Println("Unable to remove htpasswd:", err)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+ c.AbortWithStatus(http.StatusOK)
+ })
+ router.GET("/htpasswd.apr1", func(c *gin.Context) {
+ ret, err := genHtpasswd(false)
+ if err != nil {
+ c.AbortWithError(http.StatusInternalServerError, err)
+ return
+ }
+ c.String(http.StatusOK, ret)
+ })
+ router.GET("/ca", infoCA)
+ router.GET("/ca.pem", getCAPEM)
+ router.POST("/ca/new", func(c *gin.Context) {
+ var upki PKISettings
+ err := c.ShouldBindJSON(&upki)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ if err := pki.GenerateCA(upki.NotBefore, upki.NotAfter); err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusCreated, true)
+ })
+
+ router.GET("/certs", getCertificates)
+ router.POST("/certs", generateClientCert)
+ router.DELETE("/certs", func(c *gin.Context) {
+ v, err := fic.ClearCertificates()
+ if err != nil {
+ log.Println("Unable to ClearCertificates:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, v)
+ })
+
+ apiCertificatesRoutes := router.Group("/certs/:certid")
+ apiCertificatesRoutes.Use(CertificateHandler)
+ apiCertificatesRoutes.HEAD("", getTeamP12File)
+ apiCertificatesRoutes.GET("", getTeamP12File)
+ apiCertificatesRoutes.PUT("", updateCertificateAssociation)
+ apiCertificatesRoutes.DELETE("", func(c *gin.Context) {
+ cert := c.MustGet("cert").(*fic.Certificate)
+
+ v, err := cert.Revoke()
+ if err != nil {
+ log.Println("Unable to Revoke:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, v)
+ })
}
-func genCA(args map[string]string, body []byte) (interface{}, error) {
- return fic.GenerateCA(), nil
+func declareTeamCertificateRoutes(router *gin.RouterGroup) {
+ router.GET("/certificates", func(c *gin.Context) {
+ team := c.MustGet("team").(*fic.Team)
+
+ if serials, err := pki.GetTeamSerials(TeamsDir, team.Id); err != nil {
+ log.Println("Unable to GetTeamSerials:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ } else {
+ var certs []CertExported
+ for _, serial := range serials {
+ if cert, err := fic.GetCertificate(serial); err == nil {
+ certs = append(certs, CertExported{fmt.Sprintf("%0[2]*[1]X", cert.Id, int(math.Ceil(math.Log2(float64(cert.Id))/8)*2)), cert.Creation, cert.Password, &team.Id, cert.Revoked})
+ } else {
+ log.Println("Unable to get back certificate, whereas an association exists on disk: ", err)
+ }
+ }
+ c.JSON(http.StatusOK, certs)
+ }
+ })
+
+ router.GET("/associations", func(c *gin.Context) {
+ team := c.MustGet("team").(*fic.Team)
+
+ assocs, err := pki.GetTeamAssociations(TeamsDir, team.Id)
+ if err != nil {
+ log.Println("Unable to GetTeamAssociations:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, assocs)
+ })
+
+ apiTeamAssociationsRoutes := router.Group("/associations/:assoc")
+ apiTeamAssociationsRoutes.POST("", func(c *gin.Context) {
+ team := c.MustGet("team").(*fic.Team)
+
+ if err := os.Symlink(fmt.Sprintf("%d", team.Id), path.Join(TeamsDir, c.Params.ByName("assoc"))); err != nil {
+ log.Println("Unable to create association symlink:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to create association symlink: %s", err.Error())})
+ return
+ }
+
+ c.JSON(http.StatusOK, c.Params.ByName("assoc"))
+ })
+ apiTeamAssociationsRoutes.DELETE("", func(c *gin.Context) {
+ err := pki.DeleteTeamAssociation(TeamsDir, c.Params.ByName("assoc"))
+ if err != nil {
+ log.Printf("Unable to DeleteTeamAssociation(%s): %s", c.Params.ByName("assoc"), err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to delete association symlink: %s", err.Error())})
+ return
+ }
+
+ c.JSON(http.StatusOK, nil)
+ })
+
}
-func GetTeamCertificate(team fic.Team, args map[string]string, body []byte) (interface{}, error) {
-if fd, err := os.Open("../PKI/pkcs/" + team.Name + ".p12"); err == nil {
- return ioutil.ReadAll(fd)
+func CertificateHandler(c *gin.Context) {
+ var certid uint64
+ var err error
+
+ cid := strings.TrimSuffix(string(c.Params.ByName("certid")), ".p12")
+ if certid, err = strconv.ParseUint(cid, 10, 64); err != nil {
+ if certid, err = strconv.ParseUint(cid, 16, 64); err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid certficate identifier"})
+ return
+ }
+ }
+
+ cert, err := fic.GetCertificate(certid)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Certificate not found"})
+ return
+ }
+
+ c.Set("cert", cert)
+
+ c.Next()
+}
+
+func genHtpasswd(ssha bool) (ret string, err error) {
+ var teams []*fic.Team
+ teams, err = fic.GetTeams()
+ if err != nil {
+ return
+ }
+
+ for _, team := range teams {
+ var serials []uint64
+ serials, err = pki.GetTeamSerials(TeamsDir, team.Id)
+ if err != nil {
+ return
+ }
+
+ if len(serials) == 0 {
+ // Don't include teams that don't have associated certificates
+ continue
+ }
+
+ for _, serial := range serials {
+ var cert *fic.Certificate
+ cert, err = fic.GetCertificate(serial)
+ if err != nil {
+ // Ignore invalid/incorrect/non-existant certificates
+ continue
+ }
+
+ if cert.Revoked != nil {
+ continue
+ }
+
+ salt := make([]byte, 5)
+ if _, err = rand.Read(salt); err != nil {
+ return
+ }
+
+ if ssha {
+ hash := sha1.New()
+ hash.Write([]byte(cert.Password))
+ hash.Write([]byte(salt))
+
+ passwdline := fmt.Sprintf(":{SSHA}%s\n", base64.StdEncoding.EncodeToString(append(hash.Sum(nil), salt...)))
+
+ ret += strings.ToLower(team.Name) + passwdline
+ ret += fmt.Sprintf("%0[2]*[1]x", cert.Id, int(math.Ceil(math.Log2(float64(cert.Id))/8)*2)) + passwdline
+ ret += fmt.Sprintf("%0[2]*[1]X", cert.Id, int(math.Ceil(math.Log2(float64(cert.Id))/8)*2)) + passwdline
+ teamAssociations, _ := pki.GetTeamAssociations(TeamsDir, team.Id)
+ log.Println(path.Join(TeamsDir, fmt.Sprintf("%d", team.Id)), teamAssociations)
+ for _, ta := range teamAssociations {
+ ret += strings.Replace(ta, ":", "", -1) + passwdline
+ }
+ } else {
+ salt32 := base32.StdEncoding.EncodeToString(salt)
+ ret += fmt.Sprintf(
+ "%s:$apr1$%s$%s\n",
+ strings.ToLower(team.Name),
+ salt32,
+ fic.Apr1Md5(cert.Password, salt32),
+ )
+ }
+ }
+ }
+
+ return
+}
+
+type PKISettings struct {
+ Version int `json:"version"`
+ SerialNumber *big.Int `json:"serialnumber"`
+ Issuer pkix.Name `json:"issuer"`
+ Subject pkix.Name `json:"subject"`
+ NotBefore time.Time `json:"notbefore"`
+ NotAfter time.Time `json:"notafter"`
+ SignatureAlgorithm x509.SignatureAlgorithm `json:"signatureAlgorithm,"`
+ PublicKeyAlgorithm x509.PublicKeyAlgorithm `json:"publicKeyAlgorithm"`
+}
+
+func infoCA(c *gin.Context) {
+ _, cacert, err := pki.LoadCA()
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "CA not found"})
+ return
+ }
+
+ c.JSON(http.StatusOK, PKISettings{
+ Version: cacert.Version,
+ SerialNumber: cacert.SerialNumber,
+ Issuer: cacert.Issuer,
+ Subject: cacert.Subject,
+ NotBefore: cacert.NotBefore,
+ NotAfter: cacert.NotAfter,
+ SignatureAlgorithm: cacert.SignatureAlgorithm,
+ PublicKeyAlgorithm: cacert.PublicKeyAlgorithm,
+ })
+}
+
+func getCAPEM(c *gin.Context) {
+ if _, err := os.Stat(pki.CACertPath()); os.IsNotExist(err) {
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Unable to locate the CA root certificate. Have you generated it?"})
+ return
+ } else if fd, err := os.Open(pki.CACertPath()); err != nil {
+ log.Println("Unable to open CA root certificate:", err)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
} else {
- return nil, err
+ defer fd.Close()
+
+ cnt, err := ioutil.ReadAll(fd)
+ if err != nil {
+ log.Println("Unable to read CA root certificate:", err)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ c.String(http.StatusOK, string(cnt))
}
}
+
+func getTeamP12File(c *gin.Context) {
+ cert := c.MustGet("cert").(*fic.Certificate)
+
+ // Create p12 if necessary
+ if _, err := os.Stat(pki.ClientP12Path(cert.Id)); os.IsNotExist(err) {
+ if err := pki.WriteP12(cert.Id, cert.Password); err != nil {
+ log.Println("Unable to WriteP12:", err.Error())
+ c.AbortWithError(http.StatusInternalServerError, err)
+ return
+ }
+ }
+
+ if _, err := os.Stat(pki.ClientP12Path(cert.Id)); os.IsNotExist(err) {
+ log.Println("Unable to compute ClientP12Path:", err.Error())
+ c.AbortWithError(http.StatusInternalServerError, errors.New("Unable to locate the p12. Have you generated it?"))
+ return
+ } else if fd, err := os.Open(pki.ClientP12Path(cert.Id)); err != nil {
+ log.Println("Unable to open ClientP12Path:", err.Error())
+ c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("Unable to open the p12: %w", err))
+ return
+ } else {
+ defer fd.Close()
+
+ data, err := ioutil.ReadAll(fd)
+ if err != nil {
+ log.Println("Unable to open ClientP12Path:", err.Error())
+ c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("Unable to open the p12: %w", err))
+ return
+ }
+
+ c.Data(http.StatusOK, "application/x-pkcs12", data)
+ }
+}
+
+func generateClientCert(c *gin.Context) {
+ // First, generate a new, unique, serial
+ var serial_gen [8]byte
+ if _, err := rand.Read(serial_gen[:]); err != nil {
+ log.Println("Unable to read enough entropy to generate client certificate:", err)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to read enough entropy"})
+ return
+ }
+ for fic.ExistingCertSerial(serial_gen) {
+ if _, err := rand.Read(serial_gen[:]); err != nil {
+ log.Println("Unable to read enough entropy to generate client certificate:", err)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to read enough entropy"})
+ return
+ }
+ }
+
+ var serial_b big.Int
+ serial_b.SetBytes(serial_gen[:])
+ serial := serial_b.Uint64()
+
+ // Let's pick a random password
+ password, err := fic.GeneratePassword()
+ if err != nil {
+ log.Println("Unable to generate password:", err)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to generate password: " + err.Error()})
+ return
+ }
+
+ // Ok, now load CA
+ capriv, cacert, err := pki.LoadCA()
+ if err != nil {
+ log.Println("Unable to load the CA:", err)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to load the CA"})
+ return
+ }
+
+ // Generate our privkey
+ if err := pki.GenerateClient(serial, cacert.NotBefore, cacert.NotAfter, &cacert, &capriv); err != nil {
+ log.Println("Unable to generate private key:", err)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to generate private key: " + err.Error()})
+ return
+ }
+
+ // Save in DB
+ cert, err := fic.RegisterCertificate(serial, password)
+ if err != nil {
+ log.Println("Unable to register certificate:", err)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to register certificate."})
+ return
+ }
+
+ c.JSON(http.StatusOK, CertExported{fmt.Sprintf("%0[2]*[1]X", cert.Id, int(math.Ceil(math.Log2(float64(cert.Id))/8)*2)), cert.Creation, cert.Password, nil, cert.Revoked})
+}
+
+type CertExported struct {
+ Id string `json:"id"`
+ Creation time.Time `json:"creation"`
+ Password string `json:"password,omitempty"`
+ IdTeam *int64 `json:"id_team"`
+ Revoked *time.Time `json:"revoked"`
+}
+
+func getCertificates(c *gin.Context) {
+ certificates, err := fic.GetCertificates()
+ if err != nil {
+ log.Println("Unable to retrieve certificates list:", err)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during certificates retrieval."})
+ return
+ }
+ ret := make([]CertExported, 0)
+ for _, cert := range certificates {
+ dstLinkPath := path.Join(TeamsDir, pki.GetCertificateAssociation(cert.Id))
+
+ var idTeam *int64 = nil
+ if lnk, err := os.Readlink(dstLinkPath); err == nil {
+ if tid, err := strconv.ParseInt(lnk, 10, 64); err == nil {
+ idTeam = &tid
+ }
+ }
+
+ ret = append(ret, CertExported{fmt.Sprintf("%0[2]*[1]X", cert.Id, int(math.Ceil(math.Log2(float64(cert.Id))/8)*2)), cert.Creation, "", idTeam, cert.Revoked})
+ }
+
+ c.JSON(http.StatusOK, ret)
+}
+
+type CertUploaded struct {
+ Team *int64 `json:"id_team"`
+}
+
+func updateCertificateAssociation(c *gin.Context) {
+ cert := c.MustGet("cert").(*fic.Certificate)
+
+ var uc CertUploaded
+ err := c.ShouldBindJSON(&uc)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ dstLinkPath := path.Join(TeamsDir, pki.GetCertificateAssociation(cert.Id))
+
+ if uc.Team != nil {
+ srcLinkPath := fmt.Sprintf("%d", *uc.Team)
+ if err := os.Symlink(srcLinkPath, dstLinkPath); err != nil {
+ log.Println("Unable to create certificate symlink:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to create certificate symlink: %s", err.Error())})
+ return
+ }
+
+ // Mark team as active to ensure it'll be generated
+ if ut, err := fic.GetTeam(*uc.Team); err != nil {
+ log.Println("Unable to GetTeam:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during team retrieval."})
+ return
+ } else if !ut.Active {
+ ut.Active = true
+ _, err := ut.Update()
+ if err != nil {
+ log.Println("Unable to UpdateTeam after updateCertificateAssociation:", err.Error())
+ }
+ }
+ } else {
+ os.Remove(dstLinkPath)
+ }
+
+ c.JSON(http.StatusOK, cert)
+}
diff --git a/admin/api/claim.go b/admin/api/claim.go
new file mode 100644
index 00000000..cf545fff
--- /dev/null
+++ b/admin/api/claim.go
@@ -0,0 +1,499 @@
+package api
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "path"
+ "strconv"
+ "time"
+
+ "srs.epita.fr/fic-server/admin/generation"
+ "srs.epita.fr/fic-server/libfic"
+
+ "github.com/gin-gonic/gin"
+)
+
+func declareClaimsRoutes(router *gin.RouterGroup) {
+ // Tasks
+ router.GET("/claims", getClaims)
+ router.POST("/claims", newClaim)
+ router.DELETE("/claims", clearClaims)
+
+ apiClaimsRoutes := router.Group("/claims/:cid")
+ apiClaimsRoutes.Use(ClaimHandler)
+ apiClaimsRoutes.GET("", showClaim)
+ apiClaimsRoutes.PUT("", updateClaim)
+ apiClaimsRoutes.POST("", addClaimDescription)
+ apiClaimsRoutes.DELETE("", deleteClaim)
+
+ apiClaimsRoutes.GET("/last_update", getClaimLastUpdate)
+ apiClaimsRoutes.PUT("/descriptions", updateClaimDescription)
+
+ // Assignees
+ router.GET("/claims-assignees", getAssignees)
+ router.POST("/claims-assignees", newAssignee)
+
+ apiClaimAssigneesRoutes := router.Group("/claims-assignees/:aid")
+ apiClaimAssigneesRoutes.Use(ClaimAssigneeHandler)
+ router.GET("/claims-assignees/:aid", showClaimAssignee)
+ router.PUT("/claims-assignees/:aid", updateClaimAssignee)
+ router.DELETE("/claims-assignees/:aid", deleteClaimAssignee)
+}
+
+func declareExerciceClaimsRoutes(router *gin.RouterGroup) {
+ router.GET("/claims", getExerciceClaims)
+}
+
+func declareTeamClaimsRoutes(router *gin.RouterGroup) {
+ router.GET("/api/teams/:tid/issue.json", func(c *gin.Context) {
+ team := c.MustGet("team").(*fic.Team)
+
+ issues, err := team.MyIssueFile()
+ if err != nil {
+ log.Printf("Unable to MyIssueFile(tid=%d): %s", team.Id, err.Error())
+ c.JSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to generate issues.json."})
+ return
+ }
+
+ c.JSON(http.StatusOK, issues)
+ })
+
+ router.GET("/claims", getTeamClaims)
+}
+
+func ClaimHandler(c *gin.Context) {
+ cid, err := strconv.ParseInt(string(c.Params.ByName("cid")), 10, 64)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid claim identifier"})
+ return
+ }
+
+ claim, err := fic.GetClaim(cid)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Requested claim not found"})
+ return
+ }
+
+ c.Set("claim", claim)
+
+ c.Next()
+}
+
+func ClaimAssigneeHandler(c *gin.Context) {
+ aid, err := strconv.ParseInt(string(c.Params.ByName("aid")), 10, 64)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid claim assignee identifier"})
+ return
+ }
+
+ assignee, err := fic.GetAssignee(aid)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Requested claim-assignee not found"})
+ return
+ }
+
+ c.Set("claim-assignee", assignee)
+
+ c.Next()
+}
+
+func getClaims(c *gin.Context) {
+ claims, err := fic.GetClaims()
+
+ if err != nil {
+ log.Println("Unable to getClaims:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during claims retrieval."})
+ return
+ }
+
+ c.JSON(http.StatusOK, claims)
+}
+
+func getTeamClaims(c *gin.Context) {
+ team := c.MustGet("team").(*fic.Team)
+
+ claims, err := team.GetClaims()
+ if err != nil {
+ log.Printf("Unable to GetClaims(tid=%d): %s", team.Id, err.Error())
+ c.JSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve claim list."})
+ return
+ }
+
+ c.JSON(http.StatusOK, claims)
+}
+
+func getExerciceClaims(c *gin.Context) {
+ exercice := c.MustGet("exercice").(*fic.Exercice)
+
+ claims, err := exercice.GetClaims()
+ if err != nil {
+ log.Printf("Unable to GetClaims(eid=%d): %s", exercice.Id, err.Error())
+ c.JSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve claim list."})
+ return
+ }
+
+ c.JSON(http.StatusOK, claims)
+}
+
+func getClaimLastUpdate(c *gin.Context) {
+ claim := c.MustGet("claim").(*fic.Claim)
+
+ v, err := claim.GetLastUpdate()
+ if err != nil {
+ log.Printf("Unable to GetLastUpdate: %s", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during claim last update retrieval."})
+ return
+ }
+
+ c.JSON(http.StatusOK, v)
+}
+
+type ClaimExported struct {
+ Id int64 `json:"id"`
+ Subject string `json:"subject"`
+ IdTeam *int64 `json:"id_team"`
+ Team *fic.Team `json:"team"`
+ IdExercice *int64 `json:"id_exercice"`
+ Exercice *fic.Exercice `json:"exercice"`
+ IdAssignee *int64 `json:"id_assignee"`
+ Assignee *fic.ClaimAssignee `json:"assignee"`
+ Creation time.Time `json:"creation"`
+ LastUpdate time.Time `json:"last_update"`
+ State string `json:"state"`
+ Priority string `json:"priority"`
+ Descriptions []*fic.ClaimDescription `json:"descriptions"`
+}
+
+func showClaim(c *gin.Context) {
+ claim := c.MustGet("claim").(*fic.Claim)
+
+ var e ClaimExported
+ var err error
+ if e.Team, err = claim.GetTeam(); err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Unable to find associated team: %s", err.Error())})
+ return
+ }
+ if e.Exercice, err = claim.GetExercice(); err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Unable to find associated exercice: %s", err.Error())})
+ return
+ }
+ if e.Assignee, err = claim.GetAssignee(); err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Unable to find associated assignee: %s", err.Error())})
+ return
+ }
+ if e.Descriptions, err = claim.GetDescriptions(); err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Unable to find claim's descriptions: %s", err.Error())})
+ return
+ }
+
+ e.LastUpdate = e.Creation
+ for _, d := range e.Descriptions {
+ if d.Date.After(e.LastUpdate) {
+ e.LastUpdate = d.Date
+ }
+ }
+
+ e.Id = claim.Id
+ e.IdAssignee = claim.IdAssignee
+ e.IdTeam = claim.IdTeam
+ e.IdExercice = claim.IdExercice
+ e.Subject = claim.Subject
+ e.Creation = claim.Creation
+ e.State = claim.State
+ e.Priority = claim.Priority
+
+ c.JSON(http.StatusOK, e)
+}
+
+type ClaimUploaded struct {
+ fic.Claim
+ Whoami *int64 `json:"whoami"`
+}
+
+func newClaim(c *gin.Context) {
+ var uc ClaimUploaded
+ err := c.ShouldBindJSON(&uc)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ if uc.Subject == "" {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Claim's subject cannot be empty."})
+ return
+ }
+
+ var t *fic.Team
+ if uc.IdTeam != nil {
+ if team, err := fic.GetTeam(*uc.IdTeam); err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Unable to get associated team: %s", err.Error())})
+ return
+ } else {
+ t = team
+ }
+ } else {
+ t = nil
+ }
+
+ var e *fic.Exercice
+ if uc.IdExercice != nil {
+ if exercice, err := fic.GetExercice(*uc.IdExercice); err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Unable to get associated exercice: %s", err.Error())})
+ return
+ } else {
+ e = exercice
+ }
+ } else {
+ e = nil
+ }
+
+ var a *fic.ClaimAssignee
+ if uc.IdAssignee != nil {
+ if assignee, err := fic.GetAssignee(*uc.IdAssignee); err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Unable to get associated assignee: %s", err.Error())})
+ return
+ } else {
+ a = assignee
+ }
+ } else {
+ a = nil
+ }
+
+ if uc.Priority == "" {
+ uc.Priority = "medium"
+ }
+
+ claim, err := fic.NewClaim(uc.Subject, t, e, a, uc.Priority)
+ if err != nil {
+ log.Println("Unable to newClaim:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to register new claim"})
+ return
+ }
+
+ c.JSON(http.StatusOK, claim)
+}
+
+func clearClaims(c *gin.Context) {
+ nb, err := fic.ClearClaims()
+ if err != nil {
+ log.Printf("Unable to clearClaims: %s", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during claims clearing."})
+ return
+ }
+
+ c.JSON(http.StatusOK, nb)
+}
+
+func generateTeamIssuesFile(team fic.Team) error {
+ if generation.GeneratorSocket == "" {
+ if my, err := team.MyIssueFile(); err != nil {
+ return fmt.Errorf("Unable to generate issue FILE (tid=%d): %w", team.Id, err)
+ } else if j, err := json.Marshal(my); err != nil {
+ return fmt.Errorf("Unable to encode issues' file JSON: %w", err)
+ } else if err = ioutil.WriteFile(path.Join(TeamsDir, fmt.Sprintf("%d", team.Id), "issues.json"), j, 0644); err != nil {
+ return fmt.Errorf("Unable to write issues' file: %w", err)
+ }
+ } else {
+ resp, err := generation.PerformGeneration(fic.GenStruct{Type: fic.GenTeamIssues, TeamId: team.Id})
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ v, _ := ioutil.ReadAll(resp.Body)
+ return fmt.Errorf("%s", string(v))
+ }
+ }
+ return nil
+}
+
+func addClaimDescription(c *gin.Context) {
+ claim := c.MustGet("claim").(*fic.Claim)
+
+ var ud fic.ClaimDescription
+ err := c.ShouldBindJSON(&ud)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ assignee, err := fic.GetAssignee(ud.IdAssignee)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Unable to get associated assignee: %s", err.Error())})
+ return
+ }
+
+ description, err := claim.AddDescription(ud.Content, assignee, ud.Publish)
+ if err != nil {
+ log.Println("Unable to addClaimDescription:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to add description"})
+ return
+ }
+
+ if team, _ := claim.GetTeam(); team != nil {
+ err = generateTeamIssuesFile(*team)
+ if err != nil {
+ log.Println("Unable to generateTeamIssuesFile after addClaimDescription:", err.Error())
+ }
+ }
+
+ c.JSON(http.StatusOK, description)
+}
+
+func updateClaimDescription(c *gin.Context) {
+ claim := c.MustGet("claim").(*fic.Claim)
+
+ var ud fic.ClaimDescription
+ err := c.ShouldBindJSON(&ud)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ if _, err := ud.Update(); err != nil {
+ log.Println("Unable to updateClaimDescription:", err.Error())
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "An error occurs during claim description updating."})
+ return
+ }
+ if team, _ := claim.GetTeam(); team != nil {
+ err = generateTeamIssuesFile(*team)
+ if err != nil {
+ log.Println("Unable to generateTeamIssuesFile:", err.Error())
+ }
+ }
+
+ c.JSON(http.StatusOK, ud)
+}
+
+func updateClaim(c *gin.Context) {
+ claim := c.MustGet("claim").(*fic.Claim)
+
+ var uc ClaimUploaded
+ err := c.ShouldBindJSON(&uc)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ uc.Id = claim.Id
+
+ _, err = uc.Update()
+ if err != nil {
+ log.Printf("Unable to updateClaim: %s", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during claim update."})
+ return
+ }
+
+ if claim.State != uc.State {
+ if uc.Whoami != nil {
+ if assignee, err := fic.GetAssignee(*uc.Whoami); err == nil {
+ claim.AddDescription(fmt.Sprintf("%s a changé l'état de la tâche vers %q (était %q).", assignee.Name, uc.State, claim.State), assignee, true)
+ }
+ }
+ }
+
+ if claim.IdAssignee != uc.IdAssignee {
+ if uc.Whoami != nil {
+ if whoami, err := fic.GetAssignee(*uc.Whoami); err == nil {
+ if uc.IdAssignee != nil {
+ if assignee, err := fic.GetAssignee(*uc.IdAssignee); err == nil {
+ if assignee.Id != whoami.Id {
+ claim.AddDescription(fmt.Sprintf("%s a assigné la tâche à %s.", whoami.Name, assignee.Name), whoami, false)
+ } else {
+ claim.AddDescription(fmt.Sprintf("%s s'est assigné la tâche.", assignee.Name), whoami, false)
+ }
+ }
+ } else {
+ claim.AddDescription(fmt.Sprintf("%s a retiré l'attribution de la tâche.", whoami.Name), whoami, false)
+ }
+ }
+ }
+ }
+
+ if team, _ := claim.GetTeam(); team != nil {
+ err = generateTeamIssuesFile(*team)
+ }
+
+ c.JSON(http.StatusOK, uc)
+}
+
+func deleteClaim(c *gin.Context) {
+ claim := c.MustGet("claim").(*fic.Claim)
+
+ if nb, err := claim.Delete(); err != nil {
+ log.Println("Unable to deleteClaim:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during claim deletion."})
+ return
+ } else {
+ c.JSON(http.StatusOK, nb)
+ }
+}
+
+func getAssignees(c *gin.Context) {
+ assignees, err := fic.GetAssignees()
+ if err != nil {
+ log.Println("Unable to getAssignees:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during assignees retrieval."})
+ return
+ }
+
+ c.JSON(http.StatusOK, assignees)
+}
+
+func showClaimAssignee(c *gin.Context) {
+ c.JSON(http.StatusOK, c.MustGet("claim-assignee").(*fic.ClaimAssignee))
+}
+func newAssignee(c *gin.Context) {
+ var ua fic.ClaimAssignee
+ err := c.ShouldBindJSON(&ua)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ assignee, err := fic.NewClaimAssignee(ua.Name)
+ if err != nil {
+ log.Println("Unable to newAssignee:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during assignee creation."})
+ return
+ }
+
+ c.JSON(http.StatusOK, assignee)
+}
+
+func updateClaimAssignee(c *gin.Context) {
+ assignee := c.MustGet("claim-assignee").(*fic.ClaimAssignee)
+
+ var ua fic.ClaimAssignee
+ err := c.ShouldBindJSON(&ua)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ ua.Id = assignee.Id
+
+ if _, err := ua.Update(); err != nil {
+ log.Println("Unable to updateClaimAssignee:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during claim assignee update."})
+ return
+ }
+
+ c.JSON(http.StatusOK, ua)
+}
+
+func deleteClaimAssignee(c *gin.Context) {
+ assignee := c.MustGet("claim-assignee").(*fic.ClaimAssignee)
+
+ if _, err := assignee.Delete(); err != nil {
+ log.Println("Unable to deleteClaimAssignee:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("An error occurs during claim assignee deletion: %s", err.Error())})
+ return
+ }
+
+ c.JSON(http.StatusOK, true)
+}
diff --git a/admin/api/events.go b/admin/api/events.go
index 0a12d588..17500535 100644
--- a/admin/api/events.go
+++ b/admin/api/events.go
@@ -1,17 +1,154 @@
package api
import (
+ "encoding/json"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "path"
+ "strconv"
+
"srs.epita.fr/fic-server/libfic"
+
+ "github.com/gin-gonic/gin"
)
-func init() {
- router.Path("/events/").Methods("GET").HandlerFunc(apiHandler(getEvents))
+func declareEventsRoutes(router *gin.RouterGroup) {
+ router.GET("/events", getEvents)
+ router.GET("/events.json", getLastEvents)
+ router.POST("/events", newEvent)
+ router.DELETE("/events", clearEvents)
+
+ apiEventsRoutes := router.Group("/events/:evid")
+ apiEventsRoutes.Use(EventHandler)
+ apiEventsRoutes.GET("", showEvent)
+ apiEventsRoutes.PUT("", updateEvent)
+ apiEventsRoutes.DELETE("", deleteEvent)
}
-func getEvents(args map[string]string, body []byte) (interface{}, error) {
- if evts, err := fic.GetEvents(); err != nil {
- return nil, err
- } else {
- return evts, nil
+func EventHandler(c *gin.Context) {
+ evid, err := strconv.ParseInt(string(c.Params.ByName("evid")), 10, 64)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid event identifier"})
+ return
}
+
+ event, err := fic.GetEvent(evid)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Event not found"})
+ return
+ }
+
+ c.Set("event", event)
+
+ c.Next()
+}
+
+func genEventsFile() error {
+ if evts, err := fic.GetLastEvents(); err != nil {
+ return err
+ } else if j, err := json.Marshal(evts); err != nil {
+ return err
+ } else if err := ioutil.WriteFile(path.Join(TeamsDir, "events.json"), j, 0666); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func getEvents(c *gin.Context) {
+ evts, err := fic.GetEvents()
+ if err != nil {
+ log.Println("Unable to GetEvents:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve events list"})
+ return
+ }
+
+ c.JSON(http.StatusOK, evts)
+}
+
+func getLastEvents(c *gin.Context) {
+ evts, err := fic.GetLastEvents()
+
+ if err != nil {
+ log.Println("Unable to GetLastEvents:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve last events list"})
+ return
+ }
+
+ c.JSON(http.StatusOK, evts)
+}
+
+func newEvent(c *gin.Context) {
+ var ue fic.Event
+ err := c.ShouldBindJSON(&ue)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ event, err := fic.NewEvent(ue.Text, ue.Kind)
+ if err != nil {
+ log.Printf("Unable to newEvent: %s", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during event creation."})
+ return
+ }
+
+ genEventsFile()
+
+ c.JSON(http.StatusOK, event)
+}
+
+func clearEvents(c *gin.Context) {
+ nb, err := fic.ClearEvents()
+ if err != nil {
+ log.Printf("Unable to clearEvent: %s", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during event clearing."})
+ return
+ }
+
+ c.JSON(http.StatusOK, nb)
+}
+
+func showEvent(c *gin.Context) {
+ event := c.MustGet("event").(*fic.Event)
+ c.JSON(http.StatusOK, event)
+}
+
+func updateEvent(c *gin.Context) {
+ event := c.MustGet("event").(*fic.Event)
+
+ var ue fic.Event
+ err := c.ShouldBindJSON(&ue)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ ue.Id = event.Id
+
+ if _, err := ue.Update(); err != nil {
+ log.Printf("Unable to updateEvent: %s", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during event update."})
+ return
+ }
+
+ genEventsFile()
+
+ c.JSON(http.StatusOK, ue)
+}
+
+func deleteEvent(c *gin.Context) {
+ event := c.MustGet("event").(*fic.Event)
+
+ _, err := event.Delete()
+ if err != nil {
+ log.Printf("Unable to deleteEvent: %s", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during event deletion."})
+ return
+ }
+
+ genEventsFile()
+
+ c.JSON(http.StatusOK, true)
}
diff --git a/admin/api/exercice.go b/admin/api/exercice.go
index c4eeba7d..2c196153 100644
--- a/admin/api/exercice.go
+++ b/admin/api/exercice.go
@@ -1,136 +1,1749 @@
package api
import (
- "encoding/json"
- "errors"
+ "bytes"
+ "fmt"
+ "log"
+ "net/http"
+ "path"
+ "reflect"
"strconv"
+ "strings"
+ "time"
+ "srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic"
+
+ "github.com/gin-gonic/gin"
)
-func init() {
- router.Path("/exercices/").Methods("GET").HandlerFunc(apiHandler(listExercices))
- re := router.Path("/exercices/{eid:[0-9]+}").Subrouter()
- re.Methods("GET").HandlerFunc(apiHandler(exerciceHandler(showExercice)))
- re.Methods("PUT").HandlerFunc(apiHandler(updateExercice))
- re.Methods("DELETE").HandlerFunc(apiHandler(deleteExercice))
+func declareGlobalExercicesRoutes(router *gin.RouterGroup) {
+ router.GET("/resolutions.json", exportResolutionMovies)
+ router.GET("/exercices_stats.json", getExercicesStats)
+ router.GET("/exercices_forge_bindings.json", getExercicesForgeLinks)
+ router.GET("/tags", listTags)
}
-func listExercices(args map[string]string, body []byte) (interface{}, error) {
- // List all exercices
- return fic.GetExercices()
+func declareExercicesRoutes(router *gin.RouterGroup) {
+ router.GET("/exercices", listExercices)
+ router.POST("/exercices", createExercice)
+
+ apiExercicesRoutes := router.Group("/exercices/:eid")
+ apiExercicesRoutes.Use(ExerciceHandler)
+ apiExercicesRoutes.GET("", showExercice)
+ apiExercicesRoutes.PUT("", updateExercice)
+ apiExercicesRoutes.PATCH("", partUpdateExercice)
+ apiExercicesRoutes.DELETE("", deleteExercice)
+
+ apiExercicesRoutes.POST("/diff-sync", APIDiffExerciceWithRemote)
+
+ apiExercicesRoutes.GET("/history.json", getExerciceHistory)
+
+ apiExercicesRoutes.GET("/stats.json", getExerciceStats)
+
+ apiExercicesRoutes.GET("/tries", listTries)
+
+ apiTriesRoutes := apiExercicesRoutes.Group("/tries/:trid")
+ apiTriesRoutes.Use(ExerciceTryHandler)
+ apiTriesRoutes.GET("", getExerciceTry)
+ apiTriesRoutes.DELETE("", deleteExerciceTry)
+
+ apiHistoryRoutes := apiExercicesRoutes.Group("/history.json")
+ apiHistoryRoutes.Use(AssigneeCookieHandler)
+ apiHistoryRoutes.PUT("", appendExerciceHistory)
+ apiHistoryRoutes.PATCH("", updateExerciceHistory)
+ apiHistoryRoutes.DELETE("", delExerciceHistory)
+
+ apiExercicesRoutes.GET("/hints", listExerciceHints)
+ apiExercicesRoutes.POST("/hints", createExerciceHint)
+
+ apiHintsRoutes := apiExercicesRoutes.Group("/hints/:hid")
+ apiHintsRoutes.Use(HintHandler)
+ apiHintsRoutes.GET("", showExerciceHint)
+ apiHintsRoutes.PUT("", updateExerciceHint)
+ apiHintsRoutes.DELETE("", deleteExerciceHint)
+ apiHintsRoutes.GET("/dependancies", showExerciceHintDeps)
+
+ apiExercicesRoutes.GET("/flags", listExerciceFlags)
+ apiExercicesRoutes.POST("/flags", createExerciceFlag)
+
+ apiFlagsRoutes := apiExercicesRoutes.Group("/flags/:kid")
+ apiFlagsRoutes.Use(FlagKeyHandler)
+ apiFlagsRoutes.GET("", showExerciceFlag)
+ apiFlagsRoutes.PUT("", updateExerciceFlag)
+ apiFlagsRoutes.POST("/try", tryExerciceFlag)
+ apiFlagsRoutes.DELETE("/", deleteExerciceFlag)
+ apiFlagsRoutes.GET("/dependancies", showExerciceFlagDeps)
+ apiFlagsRoutes.GET("/statistics", showExerciceFlagStats)
+ apiFlagsRoutes.DELETE("/tries", deleteExerciceFlagTries)
+ apiFlagsRoutes.GET("/choices/", listFlagChoices)
+ apiFlagsChoicesRoutes := apiExercicesRoutes.Group("/choices/:cid")
+ apiFlagsChoicesRoutes.Use(FlagChoiceHandler)
+ apiFlagsChoicesRoutes.GET("", showFlagChoice)
+ apiFlagsRoutes.POST("/choices/", createFlagChoice)
+ apiFlagsChoicesRoutes.PUT("", updateFlagChoice)
+ apiFlagsChoicesRoutes.DELETE("", deleteFlagChoice)
+
+ apiQuizRoutes := apiExercicesRoutes.Group("/quiz/:qid")
+ apiQuizRoutes.Use(FlagQuizHandler)
+ apiExercicesRoutes.GET("/quiz", listExerciceQuiz)
+ apiQuizRoutes.GET("", showExerciceQuiz)
+ apiQuizRoutes.PUT("", updateExerciceQuiz)
+ apiQuizRoutes.DELETE("", deleteExerciceQuiz)
+ apiQuizRoutes.GET("/dependancies", showExerciceQuizDeps)
+ apiQuizRoutes.GET("/statistics", showExerciceQuizStats)
+ apiQuizRoutes.DELETE("/tries", deleteExerciceQuizTries)
+
+ apiExercicesRoutes.GET("/tags", listExerciceTags)
+ apiExercicesRoutes.POST("/tags", addExerciceTag)
+ apiExercicesRoutes.PUT("/tags", updateExerciceTags)
+
+ declareFilesRoutes(apiExercicesRoutes)
+ declareExerciceClaimsRoutes(apiExercicesRoutes)
+
+ // Remote
+ router.GET("/remote/themes/:thid/exercices/:exid", sync.ApiGetRemoteExercice)
+ router.GET("/remote/themes/:thid/exercices/:exid/flags", sync.ApiGetRemoteExerciceFlags)
+ router.GET("/remote/themes/:thid/exercices/:exid/hints", sync.ApiGetRemoteExerciceHints)
}
-func showExercice(exercice fic.Exercice, args map[string]string, body []byte) (interface{}, error) {
- return exercice, nil
+type Exercice struct {
+ *fic.Exercice
+ ForgeLink string `json:"forge_link,omitempty"`
}
-func deleteExercice(args map[string]string, body []byte) (interface{}, error) {
- if eid, err := strconv.Atoi(args["eid"]); err != nil {
- return nil, err
- } else if exercice, err := fic.GetExercice(int64(eid)); err != nil {
- return nil, err
+func ExerciceHandler(c *gin.Context) {
+ eid, err := strconv.ParseInt(string(c.Params.ByName("eid")), 10, 32)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid exercice identifier"})
+ return
+ }
+
+ var exercice *fic.Exercice
+ if theme, exists := c.Get("theme"); exists {
+ exercice, err = theme.(*fic.Theme).GetExercice(int(eid))
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Exercice not found"})
+ return
+ }
} else {
- return exercice.Delete()
+ exercice, err = fic.GetExercice(eid)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Exercice not found"})
+ return
+ }
+
+ if exercice.IdTheme != nil {
+ theme, err = exercice.GetTheme()
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to find the attached theme."})
+ return
+ }
+
+ c.Set("theme", theme)
+ } else {
+ c.Set("theme", &fic.StandaloneExercicesTheme)
+ }
+ }
+
+ c.Set("exercice", exercice)
+
+ c.Next()
+}
+
+func HintHandler(c *gin.Context) {
+ hid, err := strconv.ParseInt(string(c.Params.ByName("hid")), 10, 32)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid hint identifier"})
+ return
+ }
+
+ exercice := c.MustGet("exercice").(*fic.Exercice)
+ hint, err := exercice.GetHint(hid)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Hint not found"})
+ return
+ }
+
+ c.Set("hint", hint)
+
+ c.Next()
+}
+
+func FlagKeyHandler(c *gin.Context) {
+ kid, err := strconv.ParseInt(string(c.Params.ByName("kid")), 10, 32)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid flag identifier"})
+ return
+ }
+
+ var flag *fic.FlagKey
+ if exercice, exists := c.Get("exercice"); exists {
+ flag, err = exercice.(*fic.Exercice).GetFlagKey(int(kid))
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Flag not found"})
+ return
+ }
+ } else {
+ flag, err = fic.GetFlagKey(int(kid))
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Flag not found"})
+ return
+ }
+ }
+
+ c.Set("flag-key", flag)
+
+ c.Next()
+}
+
+func FlagChoiceHandler(c *gin.Context) {
+ cid, err := strconv.ParseInt(string(c.Params.ByName("cid")), 10, 32)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid choice identifier"})
+ return
+ }
+
+ flagkey := c.MustGet("flag-key").(*fic.FlagKey)
+ choice, err := flagkey.GetChoice(int(cid))
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Choice not found"})
+ return
+ }
+
+ c.Set("flag-choice", choice)
+
+ c.Next()
+}
+
+func FlagQuizHandler(c *gin.Context) {
+ qid, err := strconv.ParseInt(string(c.Params.ByName("qid")), 10, 64)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid quiz identifier"})
+ return
+ }
+
+ var quiz *fic.MCQ
+ if exercice, exists := c.Get("exercice"); exists {
+ quiz, err = exercice.(*fic.Exercice).GetMCQById(int(qid))
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Quiz not found"})
+ return
+ }
+ } else {
+ quiz, err = fic.GetMCQ(int(qid))
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Quiz not found"})
+ return
+ }
+ }
+
+ c.Set("flag-quiz", quiz)
+
+ c.Next()
+}
+
+func listExercices(c *gin.Context) {
+ if theme, exists := c.Get("theme"); exists {
+ exercices, err := theme.(*fic.Theme).GetExercices()
+ if err != nil {
+ log.Println("Unable to listThemedExercices:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during exercices listing."})
+ return
+ }
+
+ c.JSON(http.StatusOK, exercices)
+ } else {
+ exercices, err := fic.GetExercices()
+ if err != nil {
+ log.Println("Unable to listThemedExercices:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during exercices listing."})
+ return
+ }
+
+ c.JSON(http.StatusOK, exercices)
}
}
-type uploadedExercice struct {
- Title string
- Statement string
- Depend *int64
- Gain int
- VideoURI string
+func listTags(c *gin.Context) {
+ exercices, err := fic.GetExercices()
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ ret := map[string][]*fic.Exercice{}
+ for _, exercice := range exercices {
+ tags, err := exercice.GetTags()
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+ for _, t := range tags {
+ if _, ok := ret[t]; !ok {
+ ret[t] = []*fic.Exercice{}
+ }
+
+ ret[t] = append(ret[t], exercice)
+ }
+ }
+
+ c.JSON(http.StatusOK, ret)
}
-func updateExercice(args map[string]string, body []byte) (interface{}, error) {
- if eid, err := strconv.Atoi(args["eid"]); err != nil {
- return nil, err
- } else if exercice, err := fic.GetExercice(int64(eid)); err != nil {
+// Generate the csv to export with:
+//
+// curl -s http://127.0.0.1:8081/api/resolutions.json | jq -r ".[] | [ .theme,.level,.title, @uri \"https://fic.srs.epita.fr/$(date +%Y)/\\(.videoURI)\" ] | join(\";\")"
+func exportResolutionMovies(c *gin.Context) {
+ exercices, err := fic.GetExercices()
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ export := []map[string]string{}
+ for _, exercice := range exercices {
+ var tname string
+ if exercice.IdTheme != nil {
+ theme, err := fic.GetTheme(*exercice.IdTheme)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+ tname = theme.Name
+ }
+ if len(exercice.VideoURI) > 0 {
+ level, _ := exercice.GetLevel()
+ export = append(export, map[string]string{
+ "videoURI": strings.Replace(exercice.VideoURI, "$FILES$/", "files/", 1),
+ "theme": tname,
+ "title": exercice.Title,
+ "level": fmt.Sprintf("%d", level),
+ })
+ }
+ }
+
+ c.JSON(http.StatusOK, export)
+}
+
+func loadFlags(n func() ([]fic.Flag, error)) (interface{}, error) {
+ if flags, err := n(); err != nil {
return nil, err
} else {
- // Update an exercice
- var ue uploadedExercice
- if err := json.Unmarshal(body, &ue); err != nil {
- return nil, err
- }
+ var ret []fic.Flag
- if len(ue.Title) == 0 {
- return nil, errors.New("Exercice's title not filled")
- }
-
- if ue.Depend != nil {
- if _, err := fic.GetExercice(*ue.Depend); err != nil {
- return nil, err
+ for _, flag := range flags {
+ if f, ok := flag.(*fic.FlagKey); ok {
+ if k, err := fic.GetFlagKey(f.Id); err != nil {
+ return nil, err
+ } else {
+ ret = append(ret, k)
+ }
+ } else if f, ok := flag.(*fic.MCQ); ok {
+ if m, err := fic.GetMCQ(f.Id); err != nil {
+ return nil, err
+ } else {
+ ret = append(ret, m)
+ }
+ } else {
+ return nil, fmt.Errorf("Flag type %T not implemented for this flag.", f)
}
}
- exercice.Title = ue.Title
- exercice.Statement = ue.Statement
- exercice.Depend = ue.Depend
- exercice.Gain = int64(ue.Gain)
- exercice.VideoURI = ue.VideoURI
-
- return exercice.Update()
+ return ret, nil
}
}
-func createExercice(theme fic.Theme, args map[string]string, body []byte) (interface{}, error) {
+func listExerciceHints(c *gin.Context) {
+ exercice := c.MustGet("exercice").(*fic.Exercice)
+
+ hints, err := exercice.GetHints()
+ if err != nil {
+ log.Println("Unable to listExerciceHints:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving hints"})
+ return
+ }
+
+ c.JSON(http.StatusOK, hints)
+}
+
+func listExerciceFlags(c *gin.Context) {
+ exercice := c.MustGet("exercice").(*fic.Exercice)
+
+ flags, err := exercice.GetFlagKeys()
+ if err != nil {
+ log.Println("Unable to listExerciceFlags:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving exercice flags"})
+ return
+ }
+
+ c.JSON(http.StatusOK, flags)
+}
+
+func listFlagChoices(c *gin.Context) {
+ flag := c.MustGet("flag-key").(*fic.FlagKey)
+
+ choices, err := flag.GetChoices()
+ if err != nil {
+ log.Println("Unable to listFlagChoices:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving flag choices"})
+ return
+ }
+
+ c.JSON(http.StatusOK, choices)
+}
+
+func listExerciceQuiz(c *gin.Context) {
+ exercice := c.MustGet("exercice").(*fic.Exercice)
+
+ quiz, err := exercice.GetMCQ()
+ if err != nil {
+ log.Println("Unable to listExerciceQuiz:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving quiz list"})
+ return
+ }
+
+ c.JSON(http.StatusOK, quiz)
+}
+
+func showExercice(c *gin.Context) {
+ exercice := c.MustGet("exercice").(*fic.Exercice)
+
+ var forgelink string
+ if fli, ok := sync.GlobalImporter.(sync.ForgeLinkedImporter); ok {
+ if u, _ := fli.GetExerciceLink(exercice); u != nil {
+ forgelink = u.String()
+ }
+ }
+
+ c.JSON(http.StatusOK, Exercice{exercice, forgelink})
+}
+
+func getExerciceHistory(c *gin.Context) {
+ exercice := c.MustGet("exercice").(*fic.Exercice)
+
+ history, err := exercice.GetHistory()
+ if err != nil {
+ log.Println("Unable to getExerciceHistory:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving exercice history"})
+ return
+ }
+
+ c.JSON(http.StatusOK, history)
+}
+
+type exerciceStats struct {
+ IdExercice int64 `json:"id_exercice,omitempty"`
+ TeamTries int64 `json:"team_tries"`
+ TotalTries int64 `json:"total_tries"`
+ SolvedCount int64 `json:"solved_count"`
+ FlagSolved []int64 `json:"flag_solved"`
+ MCQSolved []int64 `json:"mcq_solved"`
+ CurrentGain int64 `json:"current_gain"`
+}
+
+func getExerciceStats(c *gin.Context) {
+ e := c.MustGet("exercice").(*fic.Exercice)
+
+ current_gain := e.Gain
+ if fic.DiscountedFactor > 0 {
+ decoted_exercice, err := fic.GetDiscountedExercice(e.Id)
+ if err == nil {
+ current_gain = decoted_exercice.Gain
+ } else {
+ log.Println("Unable to fetch decotedExercice:", err.Error())
+ }
+ }
+
+ c.JSON(http.StatusOK, exerciceStats{
+ TeamTries: e.TriedTeamCount(),
+ TotalTries: e.TriedCount(),
+ SolvedCount: e.SolvedCount(),
+ FlagSolved: e.FlagSolved(),
+ MCQSolved: e.MCQSolved(),
+ CurrentGain: current_gain,
+ })
+}
+
+func getExercicesStats(c *gin.Context) {
+ exercices, err := fic.GetDiscountedExercices()
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ ret := []exerciceStats{}
+ for _, e := range exercices {
+ ret = append(ret, exerciceStats{
+ IdExercice: e.Id,
+ TeamTries: e.TriedTeamCount(),
+ TotalTries: e.TriedCount(),
+ SolvedCount: e.SolvedCount(),
+ FlagSolved: e.FlagSolved(),
+ MCQSolved: e.MCQSolved(),
+ CurrentGain: e.Gain,
+ })
+ }
+
+ c.JSON(http.StatusOK, ret)
+}
+
+type themeForgeBinding struct {
+ ThemeName string `json:"name"`
+ ThemePath string `json:"path"`
+ ForgeLink string `json:"forge_link"`
+ Exercices []exerciceForgeBinding `json:"exercices"`
+}
+
+type exerciceForgeBinding struct {
+ ExerciceName string `json:"name"`
+ ExercicePath string `json:"path"`
+ ForgeLink string `json:"forge_link"`
+}
+
+func getExercicesForgeLinks(c *gin.Context) {
+ themes, err := fic.GetThemesExtended()
+ if err != nil {
+ log.Println("Unable to listThemes:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during themes listing."})
+ return
+ }
+
+ fli, ok := sync.GlobalImporter.(sync.ForgeLinkedImporter)
+ if !ok {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Current importer is not compatible with ForgeLinkedImporter"})
+ return
+ }
+
+ ret := []themeForgeBinding{}
+ for _, theme := range themes {
+ exercices, err := theme.GetExercices()
+ if err != nil {
+ log.Println("Unable to listExercices:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during exercice listing."})
+ return
+ }
+
+ var exlinks []exerciceForgeBinding
+ for _, exercice := range exercices {
+ var forgelink string
+ if u, _ := fli.GetExerciceLink(exercice); u != nil {
+ forgelink = u.String()
+ }
+
+ exlinks = append(exlinks, exerciceForgeBinding{
+ ExerciceName: exercice.Title,
+ ExercicePath: exercice.Path,
+ ForgeLink: forgelink,
+ })
+ }
+
+ var forgelink string
+ if u, _ := fli.GetThemeLink(theme); u != nil {
+ forgelink = u.String()
+ }
+ ret = append(ret, themeForgeBinding{
+ ThemeName: theme.Name,
+ ThemePath: theme.Path,
+ ForgeLink: forgelink,
+ Exercices: exlinks,
+ })
+ }
+
+ c.JSON(http.StatusOK, ret)
+}
+
+func AssigneeCookieHandler(c *gin.Context) {
+ myassignee, err := c.Cookie("myassignee")
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "You must be authenticated to perform this action."})
+ return
+ }
+
+ aid, err := strconv.ParseInt(myassignee, 10, 32)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "You must be authenticated to perform this action: invalid assignee identifier."})
+ return
+ }
+
+ assignee, err := fic.GetAssignee(aid)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "You must be authenticated to perform this action: assignee not found."})
+ return
+ }
+
+ c.Set("assignee", assignee)
+
+ c.Next()
+}
+
+type uploadedExerciceHistory struct {
+ IdTeam int64 `json:"team_id"`
+ Kind string
+ Time time.Time
+ Secondary *int64
+ Coeff float32
+}
+
+func appendExerciceHistory(c *gin.Context) {
+ exercice := c.MustGet("exercice").(*fic.Exercice)
+ myassignee := c.MustGet("assignee").(*fic.ClaimAssignee)
+
+ var uh uploadedExerciceHistory
+ err := c.ShouldBindJSON(&uh)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ err = exercice.AppendHistoryItem(uh.IdTeam, uh.Kind, uh.Secondary)
+ if err != nil {
+ log.Println("Unable to appendExerciceHistory:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during history moditication."})
+ return
+ }
+ log.Printf("AUDIT: %s performs an history append: %s for team %d, exercice %d and optional %v", myassignee.Name, uh.Kind, uh.IdTeam, exercice.Id, uh.Secondary)
+
+ c.JSON(http.StatusOK, uh)
+}
+
+func updateExerciceHistory(c *gin.Context) {
+ exercice := c.MustGet("exercice").(*fic.Exercice)
+ myassignee := c.MustGet("assignee").(*fic.ClaimAssignee)
+
+ var uh uploadedExerciceHistory
+ err := c.ShouldBindJSON(&uh)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ _, err = exercice.UpdateHistoryItem(uh.Coeff, uh.IdTeam, uh.Kind, uh.Time, uh.Secondary)
+ if err != nil {
+ log.Println("Unable to updateExerciceHistory:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during history update."})
+ return
+ }
+ log.Printf("AUDIT: %s performs an history update: %s for team %d, exercice %d and optional %v, with coeff %f", myassignee.Name, uh.Kind, uh.IdTeam, exercice.Id, uh.Secondary, uh.Coeff)
+
+ c.JSON(http.StatusOK, uh)
+}
+
+func delExerciceHistory(c *gin.Context) {
+ exercice := c.MustGet("exercice").(*fic.Exercice)
+ myassignee := c.MustGet("assignee").(*fic.ClaimAssignee)
+
+ var uh uploadedExerciceHistory
+ err := c.ShouldBindJSON(&uh)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ _, err = exercice.DelHistoryItem(uh.IdTeam, uh.Kind, uh.Time, uh.Secondary)
+ if err != nil {
+ log.Println("Unable to delExerciceHistory:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during history deletion."})
+ return
+ }
+ log.Printf("AUDIT: %s performs an history deletion: %s for team %d, exercice %d and optional %v", myassignee.Name, uh.Kind, uh.IdTeam, exercice.Id, uh.Secondary)
+
+ c.JSON(http.StatusOK, true)
+}
+
+func deleteExercice(c *gin.Context) {
+ exercice := c.MustGet("exercice").(*fic.Exercice)
+
+ _, err := exercice.DeleteCascade()
+ if err != nil {
+ log.Println("Unable to deleteExercice:", err.Error())
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "An error occurs during exercice deletion"})
+ return
+ }
+
+ c.JSON(http.StatusOK, true)
+}
+
+func updateExercice(c *gin.Context) {
+ exercice := c.MustGet("exercice").(*fic.Exercice)
+
+ var ue fic.Exercice
+ err := c.ShouldBindJSON(&ue)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ ue.Id = exercice.Id
+
+ if len(ue.Title) == 0 {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Exercice's title not filled"})
+ return
+ }
+
+ if _, err := ue.Update(); err != nil {
+ log.Println("Unable to updateExercice:", err.Error())
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "An error occurs during exercice update"})
+ return
+ }
+
+ c.JSON(http.StatusOK, ue)
+}
+
+type patchExercice struct {
+ Language *string `json:"lang,omitempty"`
+ Title *string `json:"title"`
+ Disabled *bool `json:"disabled"`
+ WIP *bool `json:"wip"`
+ URLId *string `json:"urlid"`
+ Statement *string `json:"statement"`
+ Overview *string `json:"overview"`
+ Headline *string `json:"headline"`
+ Finished *string `json:"finished"`
+ Issue *string `json:"issue"`
+ IssueKind *string `json:"issuekind"`
+ Gain *int64 `json:"gain"`
+ Coefficient *float64 `json:"coefficient"`
+}
+
+func partUpdateExercice(c *gin.Context) {
+ exercice := c.MustGet("exercice").(*fic.Exercice)
+
+ var ue patchExercice
+ err := c.ShouldBindJSON(&ue)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ for _, field := range reflect.VisibleFields(reflect.TypeOf(ue)) {
+ if !reflect.ValueOf(ue).FieldByName(field.Name).IsNil() {
+ reflect.ValueOf(exercice).Elem().FieldByName(field.Name).Set(reflect.ValueOf(reflect.ValueOf(ue).FieldByName(field.Name).Elem().Interface()))
+ }
+ }
+
+ if _, err := exercice.Update(); err != nil {
+ log.Println("Unable to partUpdateExercice:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during exercice update."})
+ return
+ }
+
+ c.JSON(http.StatusOK, exercice)
+}
+
+func createExercice(c *gin.Context) {
+ theme := c.MustGet("theme").(*fic.Theme)
+
// Create a new exercice
- var ue uploadedExercice
- if err := json.Unmarshal(body, &ue); err != nil {
- return nil, err
+ var ue fic.Exercice
+ err := c.ShouldBindJSON(&ue)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
}
if len(ue.Title) == 0 {
- return nil, errors.New("Title not filled")
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Title not filled"})
+ return
}
var depend *fic.Exercice = nil
if ue.Depend != nil {
if d, err := fic.GetExercice(*ue.Depend); err != nil {
- return nil, err
+ log.Println("Unable to createExercice:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during exercice creation."})
+ return
} else {
- depend = &d
+ depend = d
}
}
- return theme.AddExercice(ue.Title, ue.Statement, depend, ue.Gain, ue.VideoURI)
-}
-
-type uploadedKey struct {
- Name string
- Key string
-}
-
-func createExerciceKey(theme fic.Theme, exercice fic.Exercice, args map[string]string, body []byte) (interface{}, error) {
- var uk uploadedKey
- if err := json.Unmarshal(body, &uk); err != nil {
- return nil, err
+ exercice, err := theme.AddExercice(ue.Title, ue.Authors, ue.Image, ue.BackgroundColor, ue.WIP, ue.URLId, ue.Path, ue.Statement, ue.Overview, ue.Headline, depend, ue.Gain, ue.VideoURI, ue.Resolution, ue.SeeAlso, ue.Finished)
+ if err != nil {
+ log.Println("Unable to createExercice:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during exercice creation."})
+ return
}
- if len(uk.Key) == 0 {
- return nil, errors.New("Key not filled")
- }
-
- return exercice.AddRawKey(uk.Name, uk.Key)
+ c.JSON(http.StatusOK, exercice)
}
type uploadedHint struct {
- Title string
- Content string
- Cost int64
+ Title string
+ Path string
+ Content string
+ Cost int64
+ URI string
}
-func createExerciceHint(theme fic.Theme, exercice fic.Exercice, args map[string]string, body []byte) (interface{}, error) {
+func createExerciceHint(c *gin.Context) {
+ exercice := c.MustGet("exercice").(*fic.Exercice)
+
var uh uploadedHint
- if err := json.Unmarshal(body, &uh); err != nil {
+ err := c.ShouldBindJSON(&uh)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ if len(uh.Content) != 0 {
+ hint, err := exercice.AddHint(uh.Title, uh.Content, uh.Cost)
+ if err != nil {
+ log.Println("Unable to AddHint in createExerciceHint:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to add hint."})
+ return
+ }
+
+ c.JSON(http.StatusOK, hint)
+ } else if len(uh.URI) != 0 {
+ hint, err := sync.ImportFile(sync.GlobalImporter, uh.URI,
+ func(filePath string, origin string) (interface{}, error) {
+ return exercice.AddHint(uh.Title, "$FILES"+strings.TrimPrefix(filePath, fic.FilesDir), uh.Cost)
+ })
+
+ if err != nil {
+ log.Println("Unable to AddHint (after ImportFile) in createExerciceHint:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to add hint."})
+ return
+ }
+
+ c.JSON(http.StatusOK, hint)
+ } else {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Hint's content not filled"})
+ return
+ }
+}
+
+func showExerciceHint(c *gin.Context) {
+ c.JSON(http.StatusOK, c.MustGet("hint").(*fic.EHint))
+}
+
+func showExerciceHintDeps(c *gin.Context) {
+ hint := c.MustGet("hint").(*fic.EHint)
+
+ deps, err := loadFlags(hint.GetDepends)
+ if err != nil {
+ log.Println("Unable to loaddeps:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to retrieve hint dependencies."})
+ return
+ }
+
+ c.JSON(http.StatusOK, deps)
+}
+
+func updateExerciceHint(c *gin.Context) {
+ hint := c.MustGet("hint").(*fic.EHint)
+
+ var uh fic.EHint
+ err := c.ShouldBindJSON(&uh)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ uh.Id = hint.Id
+
+ if len(uh.Title) == 0 {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Hint's title not filled"})
+ return
+ }
+
+ if _, err := uh.Update(); err != nil {
+ log.Println("Unable to updateExerciceHint:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to update hint."})
+ return
+ }
+
+ c.JSON(http.StatusOK, uh)
+}
+
+func deleteExerciceHint(c *gin.Context) {
+ hint := c.MustGet("hint").(*fic.EHint)
+
+ _, err := hint.Delete()
+ if err != nil {
+ log.Println("Unable to deleteExerciceHint:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to delete hint."})
+ return
+ }
+
+ c.JSON(http.StatusOK, true)
+}
+
+type uploadedFlag struct {
+ Type string
+ Label string
+ Placeholder string
+ Help string
+ IgnoreCase bool
+ Multiline bool
+ NoTrim bool
+ CaptureRe *string `json:"capture_regexp"`
+ SortReGroups bool `json:"sort_re_grps"`
+ Flag string
+ Value []byte
+ ChoicesCost int32 `json:"choices_cost"`
+ BonusGain int32 `json:"bonus_gain"`
+}
+
+func createExerciceFlag(c *gin.Context) {
+ var uk uploadedFlag
+ err := c.ShouldBindJSON(&uk)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ if len(uk.Flag) == 0 {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Flag not filled"})
+ return
+ }
+
+ var vre *string = nil
+ if uk.CaptureRe != nil && len(*uk.CaptureRe) > 0 {
+ vre = uk.CaptureRe
+ }
+
+ exercice := c.MustGet("exercice").(*fic.Exercice)
+
+ flag, err := exercice.AddRawFlagKey(uk.Label, uk.Type, uk.Placeholder, uk.IgnoreCase, uk.NoTrim, uk.Multiline, vre, uk.SortReGroups, []byte(uk.Flag), uk.ChoicesCost, uk.BonusGain)
+ if err != nil {
+ log.Println("Unable to createExerciceFlag:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to create flag."})
+ return
+ }
+
+ c.JSON(http.StatusOK, flag)
+}
+
+func showExerciceFlag(c *gin.Context) {
+ c.JSON(http.StatusOK, c.MustGet("flag-key").(*fic.FlagKey))
+}
+
+func showExerciceFlagDeps(c *gin.Context) {
+ flag := c.MustGet("flag-key").(*fic.FlagKey)
+
+ deps, err := loadFlags(flag.GetDepends)
+ if err != nil {
+ log.Println("Unable to loaddeps:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to retrieve hint dependencies."})
+ return
+ }
+
+ c.JSON(http.StatusOK, deps)
+}
+
+func showExerciceFlagStats(c *gin.Context) {
+ exercice := c.MustGet("exercice").(*fic.Exercice)
+ flag := c.MustGet("flag-key").(*fic.FlagKey)
+
+ history, err := exercice.GetHistory()
+ if err != nil {
+ log.Println("Unable to getExerciceHistory:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving exercice history"})
+ return
+ }
+
+ var completed int64
+
+ for _, hline := range history {
+ if hline["kind"].(string) == "flag_found" {
+ if int(*hline["secondary"].(*int64)) == flag.Id {
+ completed += 1
+ }
+ }
+ }
+
+ tries, err := flag.NbTries()
+ if err != nil {
+ log.Println("Unable to nbTries:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving flag tries"})
+ return
+ }
+
+ teams, err := flag.TeamsOnIt()
+ if err != nil {
+ log.Println("Unable to teamsOnIt:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving flag related teams"})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "completed": completed,
+ "tries": tries,
+ "teams": teams,
+ })
+}
+
+func deleteExerciceFlagTries(c *gin.Context) {
+ flag := c.MustGet("flag-key").(*fic.FlagKey)
+
+ err := flag.DeleteTries()
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ c.AbortWithStatusJSON(http.StatusOK, true)
+}
+
+func tryExerciceFlag(c *gin.Context) {
+ flag := c.MustGet("flag-key").(*fic.FlagKey)
+
+ var uk uploadedFlag
+ err := c.ShouldBindJSON(&uk)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ if len(uk.Flag) == 0 {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Empty submission"})
+ return
+ }
+
+ if flag.Check([]byte(uk.Flag)) == 0 {
+ c.AbortWithStatusJSON(http.StatusOK, true)
+ return
+ }
+
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Bad submission"})
+}
+
+func updateExerciceFlag(c *gin.Context) {
+ flag := c.MustGet("flag-key").(*fic.FlagKey)
+
+ var uk uploadedFlag
+ err := c.ShouldBindJSON(&uk)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ if len(uk.Label) == 0 {
+ flag.Label = "Flag"
+ } else {
+ flag.Label = uk.Label
+ }
+
+ flag.Placeholder = uk.Placeholder
+ flag.Help = uk.Help
+ flag.IgnoreCase = uk.IgnoreCase
+ flag.Multiline = uk.Multiline
+ flag.ChoicesCost = uk.ChoicesCost
+ flag.BonusGain = uk.BonusGain
+
+ if uk.CaptureRe != nil && len(*uk.CaptureRe) > 0 {
+ if flag.CaptureRegexp != uk.CaptureRe && uk.Flag == "" {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Pour changer la capture_regexp, vous devez rentrer la réponse attendue à nouveau, car le flag doit être recalculé."})
+ return
+ }
+ flag.CaptureRegexp = uk.CaptureRe
+ } else {
+ if flag.CaptureRegexp != nil && uk.Flag == "" {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Pour changer la capture_regexp, vous devez rentrer la réponse attendue à nouveau, car le flag doit être recalculé."})
+ return
+ }
+ flag.CaptureRegexp = nil
+ }
+
+ if len(uk.Flag) > 0 {
+ var err error
+ flag.Checksum, err = flag.ComputeChecksum([]byte(uk.Flag))
+ if err != nil {
+ log.Println("Unable to ComputeChecksum:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to compute flag checksum"})
+ return
+ }
+ } else {
+ flag.Checksum = uk.Value
+ }
+
+ if _, err := flag.Update(); err != nil {
+ log.Println("Unable to updateExerciceFlag:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to update flag."})
+ return
+ }
+
+ c.JSON(http.StatusOK, flag)
+}
+
+func deleteExerciceFlag(c *gin.Context) {
+ flag := c.MustGet("flag-key").(*fic.FlagKey)
+
+ _, err := flag.Delete()
+ if err != nil {
+ log.Println("Unable to deleteExerciceFlag:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to delete flag."})
+ return
+ }
+
+ c.JSON(http.StatusOK, true)
+}
+
+func createFlagChoice(c *gin.Context) {
+ flag := c.MustGet("flag-key").(*fic.FlagKey)
+
+ var uc fic.FlagChoice
+ err := c.ShouldBindJSON(&uc)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ if len(uc.Label) == 0 {
+ uc.Label = uc.Value
+ }
+
+ choice, err := flag.AddChoice(&uc)
+ if err != nil {
+ log.Println("Unable to createFlagChoice:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to create flag choice."})
+ return
+ }
+
+ c.JSON(http.StatusOK, choice)
+}
+
+func showFlagChoice(c *gin.Context) {
+ c.JSON(http.StatusOK, c.MustGet("flag-choice").(*fic.FlagChoice))
+}
+
+func updateFlagChoice(c *gin.Context) {
+ choice := c.MustGet("flag-choice").(*fic.FlagChoice)
+
+ var uc fic.FlagChoice
+ err := c.ShouldBindJSON(&uc)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ if len(uc.Label) == 0 {
+ choice.Label = uc.Value
+ } else {
+ choice.Label = uc.Label
+ }
+
+ choice.Value = uc.Value
+
+ if _, err := choice.Update(); err != nil {
+ log.Println("Unable to updateFlagChoice:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to update flag choice."})
+ return
+ }
+
+ c.JSON(http.StatusOK, choice)
+}
+
+func deleteFlagChoice(c *gin.Context) {
+ choice := c.MustGet("flag-choice").(*fic.FlagChoice)
+
+ _, err := choice.Delete()
+ if err != nil {
+ log.Println("Unable to deleteExerciceChoice:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to delete choice."})
+ return
+ }
+
+ c.JSON(http.StatusOK, true)
+}
+
+func showExerciceQuiz(c *gin.Context) {
+ c.JSON(http.StatusOK, c.MustGet("flag-quiz").(*fic.MCQ))
+}
+
+func showExerciceQuizDeps(c *gin.Context) {
+ quiz := c.MustGet("flag-quiz").(*fic.MCQ)
+
+ deps, err := loadFlags(quiz.GetDepends)
+ if err != nil {
+ log.Println("Unable to loaddeps:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to retrieve quiz dependencies."})
+ return
+ }
+
+ c.JSON(http.StatusOK, deps)
+}
+
+func showExerciceQuizStats(c *gin.Context) {
+ exercice := c.MustGet("exercice").(*fic.Exercice)
+ quiz := c.MustGet("flag-quiz").(*fic.MCQ)
+
+ history, err := exercice.GetHistory()
+ if err != nil {
+ log.Println("Unable to getExerciceHistory:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving exercice history"})
+ return
+ }
+
+ var completed int64
+
+ for _, hline := range history {
+ if hline["kind"].(string) == "mcq_found" {
+ if *hline["secondary"].(*int) == quiz.Id {
+ completed += 1
+ }
+ }
+ }
+
+ tries, err := quiz.NbTries()
+ if err != nil {
+ log.Println("Unable to nbTries:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving flag tries"})
+ return
+ }
+
+ teams, err := quiz.TeamsOnIt()
+ if err != nil {
+ log.Println("Unable to teamsOnIt:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving flag related teams"})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "completed": completed,
+ "tries": tries,
+ "teams": teams,
+ })
+}
+
+func deleteExerciceQuizTries(c *gin.Context) {
+ quiz := c.MustGet("flag-quiz").(*fic.MCQ)
+
+ err := quiz.DeleteTries()
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ c.AbortWithStatusJSON(http.StatusOK, true)
+}
+
+func updateExerciceQuiz(c *gin.Context) {
+ quiz := c.MustGet("flag-quiz").(*fic.MCQ)
+
+ var uq fic.MCQ
+ err := c.ShouldBindJSON(&uq)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ quiz.Title = uq.Title
+
+ if _, err := quiz.Update(); err != nil {
+ log.Println("Unable to updateExerciceQuiz:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to update quiz."})
+ return
+ }
+
+ // Update and remove old entries
+ var delete []int
+ for i, cur := range quiz.Entries {
+ seen := false
+ for _, next := range uq.Entries {
+ if cur.Id == next.Id {
+ seen = true
+
+ if cur.Label != next.Label || cur.Response != next.Response {
+ cur.Label = next.Label
+ cur.Response = next.Response
+ if _, err := cur.Update(); err != nil {
+ log.Println("Unable to update MCQ entry:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to update some MCQ entry"})
+ return
+ }
+ }
+
+ break
+ }
+ }
+
+ if seen == false {
+ if _, err := cur.Delete(); err != nil {
+ log.Println("Unable to delete MCQ entry:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to delete some MCQ entry"})
+ return
+ } else {
+ delete = append(delete, i)
+ }
+ }
+ }
+ for n, i := range delete {
+ quiz.Entries = append(quiz.Entries[:i-n-1], quiz.Entries[:i-n+1]...)
+ }
+
+ // Add new choices
+ for _, choice := range uq.Entries {
+ if choice.Id == 0 {
+ if ch, err := quiz.AddEntry(choice); err != nil {
+ log.Println("Unable to add MCQ entry:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to add some MCQ entry"})
+ return
+ } else {
+ quiz.Entries = append(quiz.Entries, ch)
+ }
+ }
+ }
+
+ c.JSON(http.StatusOK, quiz)
+}
+
+func deleteExerciceQuiz(c *gin.Context) {
+ quiz := c.MustGet("flag-quiz").(*fic.MCQ)
+
+ for _, choice := range quiz.Entries {
+ if _, err := choice.Delete(); err != nil {
+ log.Println("Unable to deleteExerciceQuiz (entry):", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to delete quiz entry."})
+ return
+ }
+ }
+
+ _, err := quiz.Delete()
+ if err != nil {
+ log.Println("Unable to deleteExerciceQuiz:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to delete quiz."})
+ return
+ }
+
+ c.JSON(http.StatusOK, true)
+}
+
+func listExerciceTags(c *gin.Context) {
+ exercice := c.MustGet("exercice").(*fic.Exercice)
+
+ tags, err := exercice.GetTags()
+ if err != nil {
+ log.Println("Unable to listExerciceTags:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to get tags."})
+ return
+ }
+
+ c.JSON(http.StatusOK, tags)
+}
+
+func addExerciceTag(c *gin.Context) {
+ exercice := c.MustGet("exercice").(*fic.Exercice)
+
+ var ut []string
+ err := c.ShouldBindJSON(&ut)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ // TODO: a DB transaction should be done here: on error we should rollback
+ for _, t := range ut {
+ if _, err := exercice.AddTag(t); err != nil {
+ log.Println("Unable to addExerciceTag:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to add some tag."})
+ return
+ }
+ }
+
+ c.JSON(http.StatusOK, ut)
+}
+
+func updateExerciceTags(c *gin.Context) {
+ exercice := c.MustGet("exercice").(*fic.Exercice)
+
+ exercice.WipeTags()
+ addExerciceTag(c)
+}
+
+type syncDiff struct {
+ Field string `json:"field"`
+ Link string `json:"link"`
+ Before interface{} `json:"be"`
+ After interface{} `json:"af"`
+}
+
+func diffExerciceWithRemote(exercice *fic.Exercice, theme *fic.Theme) ([]syncDiff, error) {
+ var diffs []syncDiff
+
+ // Compare exercice attributes
+ thid := exercice.Path[:strings.Index(exercice.Path, "/")]
+ exid := exercice.Path[strings.Index(exercice.Path, "/")+1:]
+ exercice_remote, err := sync.GetRemoteExercice(thid, exid, theme)
+ if err != nil {
return nil, err
}
- if len(uh.Content) == 0 {
- return nil, errors.New("Hint's content not filled")
+ for _, field := range reflect.VisibleFields(reflect.TypeOf(*exercice)) {
+ if ((field.Name == "Image") && path.Base(reflect.ValueOf(*exercice_remote).FieldByName(field.Name).String()) != path.Base(reflect.ValueOf(*exercice).FieldByName(field.Name).String())) || ((field.Name == "Depend") && (((exercice_remote.Depend == nil || exercice.Depend == nil) && exercice.Depend != exercice_remote.Depend) || (exercice_remote.Depend != nil && exercice.Depend != nil && *exercice.Depend != *exercice_remote.Depend))) || (field.Name != "Image" && field.Name != "Depend" && !reflect.ValueOf(*exercice_remote).FieldByName(field.Name).Equal(reflect.ValueOf(*exercice).FieldByName(field.Name))) {
+ if !field.IsExported() || field.Name == "Id" || field.Name == "IdTheme" || field.Name == "IssueKind" || field.Name == "Coefficient" || field.Name == "BackgroundColor" {
+ continue
+ }
+
+ diffs = append(diffs, syncDiff{
+ Field: field.Name,
+ Link: fmt.Sprintf("exercices/%d", exercice.Id),
+ Before: reflect.ValueOf(*exercice).FieldByName(field.Name).Interface(),
+ After: reflect.ValueOf(*exercice_remote).FieldByName(field.Name).Interface(),
+ })
+ }
}
- return exercice.AddHint(uh.Title, uh.Content, uh.Cost)
+ // Compare files
+ files, err := exercice.GetFiles()
+ if err != nil {
+ return nil, fmt.Errorf("Unable to GetFiles: %w", err)
+ }
+
+ files_remote, err := sync.GetRemoteExerciceFiles(thid, exid)
+ if err != nil {
+ return nil, fmt.Errorf("Unable to GetRemoteFiles: %w", err)
+ }
+
+ for i, file_remote := range files_remote {
+ if len(files) <= i {
+ diffs = append(diffs, syncDiff{
+ Field: fmt.Sprintf("files[%d]", i),
+ Link: fmt.Sprintf("exercices/%d", exercice.Id),
+ Before: nil,
+ After: file_remote,
+ })
+ continue
+ }
+
+ for _, field := range reflect.VisibleFields(reflect.TypeOf(*file_remote)) {
+ if !field.IsExported() || field.Name == "Id" || field.Name == "IdExercice" {
+ continue
+ }
+ if ((field.Name == "Path") && path.Base(reflect.ValueOf(*file_remote).FieldByName(field.Name).String()) != path.Base(reflect.ValueOf(*files[i]).FieldByName(field.Name).String())) || ((field.Name == "Checksum" || field.Name == "ChecksumShown") && !bytes.Equal(reflect.ValueOf(*file_remote).FieldByName(field.Name).Bytes(), reflect.ValueOf(*files[i]).FieldByName(field.Name).Bytes())) || (field.Name != "Checksum" && field.Name != "ChecksumShown" && field.Name != "Path" && !reflect.ValueOf(*file_remote).FieldByName(field.Name).Equal(reflect.ValueOf(*files[i]).FieldByName(field.Name))) {
+ diffs = append(diffs, syncDiff{
+ Field: fmt.Sprintf("files[%d].%s", i, field.Name),
+ Link: fmt.Sprintf("exercices/%d", exercice.Id),
+ Before: reflect.ValueOf(*files[i]).FieldByName(field.Name).Interface(),
+ After: reflect.ValueOf(*file_remote).FieldByName(field.Name).Interface(),
+ })
+ }
+ }
+ }
+
+ // Compare flags
+ flags, err := exercice.GetFlags()
+ if err != nil {
+ return nil, fmt.Errorf("Unable to GetFlags: %w", err)
+ }
+
+ flags_remote, err := sync.GetRemoteExerciceFlags(thid, exid)
+ if err != nil {
+ return nil, fmt.Errorf("Unable to GetRemoteFlags: %w", err)
+ }
+
+ var flags_not_found []interface{}
+ var flags_extra_found []interface{}
+
+ for i, flag_remote := range flags_remote {
+ if key_remote, ok := flag_remote.(*fic.FlagKey); ok {
+ found := false
+
+ for _, flag := range flags {
+ if key, ok := flag.(*fic.FlagKey); ok && (key.Label == key_remote.Label || key.Order == key_remote.Order) {
+ found = true
+
+ // Parse flag label
+ if len(key.Label) > 3 && key.Label[0] == '%' {
+ spl := strings.Split(key.Label, "%")
+ key.Label = strings.Join(spl[2:], "%")
+ }
+
+ for _, field := range reflect.VisibleFields(reflect.TypeOf(*key_remote)) {
+ if !field.IsExported() || field.Name == "Id" || field.Name == "IdExercice" {
+ continue
+ }
+ if (field.Name == "Checksum" && !bytes.Equal(key.Checksum, key_remote.Checksum)) || (field.Name == "CaptureRegexp" && ((key.CaptureRegexp == nil || key_remote.CaptureRegexp == nil) && key.CaptureRegexp != key_remote.CaptureRegexp) || (key.CaptureRegexp != nil && key_remote.CaptureRegexp != nil && *key.CaptureRegexp != *key_remote.CaptureRegexp)) || (field.Name != "Checksum" && field.Name != "CaptureRegexp" && !reflect.ValueOf(*key_remote).FieldByName(field.Name).Equal(reflect.ValueOf(*key).FieldByName(field.Name))) {
+ diffs = append(diffs, syncDiff{
+ Field: fmt.Sprintf("flags[%d].%s", i, field.Name),
+ Link: fmt.Sprintf("exercices/%d/flags#flag-%d", exercice.Id, key.Id),
+ Before: reflect.ValueOf(*key).FieldByName(field.Name).Interface(),
+ After: reflect.ValueOf(*key_remote).FieldByName(field.Name).Interface(),
+ })
+ }
+ }
+
+ break
+ }
+ }
+
+ if !found {
+ flags_not_found = append(flags_not_found, key_remote)
+ }
+ } else if mcq_remote, ok := flag_remote.(*fic.MCQ); ok {
+ found := false
+
+ for _, flag := range flags {
+ if mcq, ok := flag.(*fic.MCQ); ok && (mcq.Title == mcq_remote.Title || mcq.Order == mcq_remote.Order) {
+ found = true
+
+ for _, field := range reflect.VisibleFields(reflect.TypeOf(*mcq_remote)) {
+ if !field.IsExported() || field.Name == "Id" || field.Name == "IdExercice" {
+ continue
+ }
+ if field.Name == "Entries" {
+ var not_found []*fic.MCQ_entry
+ var extra_found []*fic.MCQ_entry
+
+ for i, entry_remote := range mcq_remote.Entries {
+ found := false
+
+ for j, entry := range mcq.Entries {
+ if entry.Label == entry_remote.Label {
+ for _, field := range reflect.VisibleFields(reflect.TypeOf(*entry_remote)) {
+ if field.Name == "Id" {
+ continue
+ }
+
+ if !reflect.ValueOf(*entry_remote).FieldByName(field.Name).Equal(reflect.ValueOf(*entry).FieldByName(field.Name)) {
+ diffs = append(diffs, syncDiff{
+ Field: fmt.Sprintf("flags[%d].entries[%d].%s", i, j, field.Name),
+ Link: fmt.Sprintf("exercices/%d/flags#quiz-%d", exercice.Id, mcq.Id),
+ Before: reflect.ValueOf(*mcq.Entries[j]).FieldByName(field.Name).Interface(),
+ After: reflect.ValueOf(*entry_remote).FieldByName(field.Name).Interface(),
+ })
+ }
+ }
+
+ found = true
+ break
+ }
+ }
+
+ if !found {
+ not_found = append(not_found, entry_remote)
+ }
+ }
+
+ for _, entry := range mcq.Entries {
+ found := false
+ for _, entry_remote := range mcq_remote.Entries {
+ if entry.Label == entry_remote.Label {
+ found = true
+ break
+ }
+ }
+
+ if !found {
+ extra_found = append(extra_found, entry)
+ }
+ }
+
+ if len(not_found) > 0 || len(extra_found) > 0 {
+ diffs = append(diffs, syncDiff{
+ Field: fmt.Sprintf("flags[%d].entries", i),
+ Link: fmt.Sprintf("exercices/%d/flags", exercice.Id),
+ Before: extra_found,
+ After: not_found,
+ })
+ }
+ } else if !reflect.ValueOf(*mcq_remote).FieldByName(field.Name).Equal(reflect.ValueOf(*mcq).FieldByName(field.Name)) {
+ diffs = append(diffs, syncDiff{
+ Field: fmt.Sprintf("flags[%d].%s", i, field.Name),
+ Link: fmt.Sprintf("exercices/%d/flags", exercice.Id),
+ Before: reflect.ValueOf(*mcq).FieldByName(field.Name).Interface(),
+ After: reflect.ValueOf(*mcq_remote).FieldByName(field.Name).Interface(),
+ })
+ }
+ }
+
+ break
+ }
+ }
+
+ if !found {
+ flags_not_found = append(flags_not_found, mcq_remote)
+ }
+ } else if label_remote, ok := flag_remote.(*fic.FlagLabel); ok {
+ found := false
+
+ for _, flag := range flags {
+ if label, ok := flag.(*fic.FlagLabel); ok && (label.Label == label_remote.Label || label.Order == label_remote.Order) {
+ found = true
+
+ for _, field := range reflect.VisibleFields(reflect.TypeOf(*label_remote)) {
+ if !field.IsExported() || field.Name == "Id" || field.Name == "IdExercice" {
+ continue
+ }
+ if !reflect.ValueOf(*label_remote).FieldByName(field.Name).Equal(reflect.ValueOf(*label).FieldByName(field.Name)) {
+ diffs = append(diffs, syncDiff{
+ Field: fmt.Sprintf("flags[%d].%s", i, field.Name),
+ Link: fmt.Sprintf("exercices/%d/flags#flag-%d", exercice.Id, label.Id),
+ Before: reflect.ValueOf(*label).FieldByName(field.Name).Interface(),
+ After: reflect.ValueOf(*label_remote).FieldByName(field.Name).Interface(),
+ })
+ }
+ }
+
+ break
+ }
+ }
+
+ if !found {
+ flags_not_found = append(flags_not_found, label_remote)
+ }
+ } else {
+ log.Printf("unknown flag type: %T", flag_remote)
+ }
+ }
+
+ for _, flag := range flags {
+ if key, ok := flag.(*fic.FlagKey); ok {
+ found := false
+
+ for _, flag_remote := range flags_remote {
+ if key_remote, ok := flag_remote.(*fic.FlagKey); ok && (key.Label == key_remote.Label || key.Order == key_remote.Order) {
+ found = true
+ break
+ }
+ }
+
+ if !found {
+ flags_extra_found = append(flags_extra_found, flag)
+ }
+ } else if mcq, ok := flag.(*fic.MCQ); ok {
+ found := false
+
+ for _, flag_remote := range flags_remote {
+ if mcq_remote, ok := flag_remote.(*fic.MCQ); ok && (mcq.Title == mcq_remote.Title || mcq.Order == mcq_remote.Order) {
+ found = true
+ break
+ }
+ }
+
+ if !found {
+ flags_extra_found = append(flags_extra_found, flag)
+ }
+ } else if label, ok := flag.(*fic.FlagLabel); ok {
+ found := false
+
+ for _, flag_remote := range flags_remote {
+ if label_remote, ok := flag_remote.(*fic.FlagLabel); ok && (label.Label == label_remote.Label || label.Order == label_remote.Order) {
+ found = true
+ break
+ }
+ }
+
+ if !found {
+ flags_extra_found = append(flags_extra_found, flag)
+ }
+ }
+ }
+
+ if len(flags_not_found) > 0 || len(flags_extra_found) > 0 {
+ diffs = append(diffs, syncDiff{
+ Field: "flags",
+ Link: fmt.Sprintf("exercices/%d/flags", exercice.Id),
+ Before: flags_extra_found,
+ After: flags_not_found,
+ })
+ }
+
+ // Compare hints
+ hints, err := exercice.GetHints()
+ if err != nil {
+ return nil, fmt.Errorf("Unable to GetHints: %w", err)
+ }
+
+ hints_remote, err := sync.GetRemoteExerciceHints(thid, exid)
+ if err != nil {
+ return nil, fmt.Errorf("Unable to GetRemoteHints: %w", err)
+ }
+
+ for i, hint_remote := range hints_remote {
+ hint_remote.Hint.TreatHintContent()
+
+ for _, field := range reflect.VisibleFields(reflect.TypeOf(*hint_remote.Hint)) {
+ if !field.IsExported() || field.Name == "Id" || field.Name == "IdExercice" {
+ continue
+ }
+ if len(hints) <= i {
+ diffs = append(diffs, syncDiff{
+ Field: fmt.Sprintf("hints[%d].%s", i, field.Name),
+ Link: fmt.Sprintf("exercices/%d", exercice.Id),
+ Before: nil,
+ After: reflect.ValueOf(*hint_remote.Hint).FieldByName(field.Name).Interface(),
+ })
+ } else if !reflect.ValueOf(*hint_remote.Hint).FieldByName(field.Name).Equal(reflect.ValueOf(*hints[i]).FieldByName(field.Name)) {
+ diffs = append(diffs, syncDiff{
+ Field: fmt.Sprintf("hints[%d].%s", i, field.Name),
+ Link: fmt.Sprintf("exercices/%d", exercice.Id),
+ Before: reflect.ValueOf(*hints[i]).FieldByName(field.Name).Interface(),
+ After: reflect.ValueOf(*hint_remote.Hint).FieldByName(field.Name).Interface(),
+ })
+ }
+ }
+ }
+
+ return diffs, err
+}
+
+func APIDiffExerciceWithRemote(c *gin.Context) {
+ theme := c.MustGet("theme").(*fic.Theme)
+ exercice := c.MustGet("exercice").(*fic.Exercice)
+
+ diffs, err := diffExerciceWithRemote(exercice, theme)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, diffs)
+}
+
+func listTries(c *gin.Context) {
+ exercice := c.MustGet("exercice").(*fic.Exercice)
+
+ tries, err := exercice.TriesList()
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, tries)
+}
+
+func ExerciceTryHandler(c *gin.Context) {
+ trid, err := strconv.ParseInt(string(c.Params.ByName("trid")), 10, 32)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid try identifier"})
+ return
+ }
+
+ exercice := c.MustGet("exercice").(*fic.Exercice)
+ try, err := exercice.GetTry(trid)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Try not found"})
+ return
+ }
+
+ c.Set("try", try)
+
+ c.Next()
+}
+
+func getExerciceTry(c *gin.Context) {
+ try := c.MustGet("try").(*fic.ExerciceTry)
+
+ err := try.FillDetails()
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, try)
+}
+
+func deleteExerciceTry(c *gin.Context) {
+ try := c.MustGet("try").(*fic.ExerciceTry)
+
+ _, err := try.Delete()
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ c.Status(http.StatusNoContent)
}
diff --git a/admin/api/export.go b/admin/api/export.go
new file mode 100644
index 00000000..aa24920a
--- /dev/null
+++ b/admin/api/export.go
@@ -0,0 +1,126 @@
+package api
+
+import (
+ "archive/zip"
+ "encoding/json"
+ "io"
+ "log"
+ "net/http"
+ "path"
+
+ "srs.epita.fr/fic-server/admin/sync"
+ "srs.epita.fr/fic-server/libfic"
+ "srs.epita.fr/fic-server/settings"
+
+ "github.com/gin-gonic/gin"
+)
+
+func declareExportRoutes(router *gin.RouterGroup) {
+ router.GET("/archive.zip", func(c *gin.Context) {
+ challengeinfo, err := GetChallengeInfo()
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ my, err := fic.MyJSONTeam(nil, true)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ s, err := settings.ReadSettings(path.Join(settings.SettingsDir, settings.SettingsFile))
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+ s.End = nil
+ s.NextChangeTime = nil
+ s.DelegatedQA = []string{}
+
+ teams, err := fic.ExportTeams(false)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ themes, err := fic.ExportThemes()
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ c.Writer.WriteHeader(http.StatusOK)
+ c.Header("Content-Disposition", "attachment; filename=archive.zip")
+ c.Header("Content-Type", "application/zip")
+
+ w := zip.NewWriter(c.Writer)
+
+ // challenge.json
+ f, err := w.Create("challenge.json")
+ if err == nil {
+ json.NewEncoder(f).Encode(challengeinfo)
+ }
+
+ // Include partners' logos from challenge.json
+ if sync.GlobalImporter != nil {
+ if len(challengeinfo.MainLogo) > 0 {
+ for _, logo := range challengeinfo.MainLogo {
+ fd, closer, err := sync.OpenOrGetFile(sync.GlobalImporter, logo)
+ if err != nil {
+ log.Printf("Unable to archive main logo %q: %s", logo, err.Error())
+ continue
+ }
+
+ f, err := w.Create(path.Join("logo", path.Base(logo)))
+ if err == nil {
+ io.Copy(f, fd)
+ }
+ closer()
+ }
+ }
+
+ if len(challengeinfo.Partners) > 0 {
+ for _, partner := range challengeinfo.Partners {
+ fd, closer, err := sync.OpenOrGetFile(sync.GlobalImporter, partner.Src)
+ if err != nil {
+ log.Printf("Unable to archive partner logo %q: %s", partner.Src, err.Error())
+ continue
+ }
+
+ f, err := w.Create(path.Join("partner", path.Base(partner.Src)))
+ if err == nil {
+ io.Copy(f, fd)
+ }
+ closer()
+ }
+ }
+ }
+
+ // my.json
+ f, err = w.Create("my.json")
+ if err == nil {
+ json.NewEncoder(f).Encode(my)
+ }
+
+ // settings.json
+ f, err = w.Create("settings.json")
+ if err == nil {
+ json.NewEncoder(f).Encode(s)
+ }
+
+ // teams.json
+ f, err = w.Create("teams.json")
+ if err == nil {
+ json.NewEncoder(f).Encode(teams)
+ }
+
+ // themes.json
+ f, err = w.Create("themes.json")
+ if err == nil {
+ json.NewEncoder(f).Encode(themes)
+ }
+
+ w.Close()
+ })
+}
diff --git a/admin/api/file.go b/admin/api/file.go
index 1678f8b7..6d94776c 100644
--- a/admin/api/file.go
+++ b/admin/api/file.go
@@ -1,120 +1,297 @@
package api
import (
- "bufio"
- "crypto/sha512"
- "encoding/base32"
- "encoding/json"
- "errors"
+ "encoding/hex"
"fmt"
"log"
"net/http"
"os"
- "path"
- "strings"
+ "path/filepath"
+ "strconv"
+ "srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic"
+
+ "github.com/gin-gonic/gin"
)
-var CloudDAVBase string
-var CloudUsername string
-var CloudPassword string
+func declareFilesGlobalRoutes(router *gin.RouterGroup) {
+ router.DELETE("/files/", clearFiles)
+
+ // Remote
+ router.GET("/remote/themes/:thid/exercices/:exid/files", sync.ApiGetRemoteExerciceFiles)
+}
+
+func declareFilesRoutes(router *gin.RouterGroup) {
+ router.GET("/files", listFiles)
+ router.POST("/files", createExerciceFile)
+
+ apiFilesRoutes := router.Group("/files/:fileid")
+ apiFilesRoutes.Use(FileHandler)
+ apiFilesRoutes.GET("", showFile)
+ apiFilesRoutes.PUT("", updateFile)
+ apiFilesRoutes.DELETE("", deleteFile)
+
+ apiFileDepsRoutes := apiFilesRoutes.Group("/dependancies/:depid")
+ apiFileDepsRoutes.Use(FileDepHandler)
+ apiFileDepsRoutes.DELETE("", deleteFileDep)
+
+ // Check
+ apiFilesRoutes.POST("/check", checkFile)
+ apiFilesRoutes.POST("/gunzip", gunzipFile)
+}
+
+func FileHandler(c *gin.Context) {
+ fileid, err := strconv.ParseInt(string(c.Params.ByName("fileid")), 10, 64)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid file identifier"})
+ return
+ }
+
+ var file *fic.EFile
+ if exercice, exists := c.Get("exercice"); exists {
+ file, err = exercice.(*fic.Exercice).GetFile(fileid)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "File not found"})
+ return
+ }
+ } else {
+ file, err = fic.GetFile(fileid)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "File not found"})
+ return
+ }
+ }
+
+ c.Set("file", file)
+
+ c.Next()
+}
+
+func FileDepHandler(c *gin.Context) {
+ depid, err := strconv.ParseInt(string(c.Params.ByName("depid")), 10, 64)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid dependency identifier"})
+ return
+ }
+
+ c.Set("file-depid", depid)
+
+ c.Next()
+}
+
+type APIFile struct {
+ *fic.EFile
+ Depends []fic.Flag `json:"depends,omitempty"`
+}
+
+func genFileList(in []*fic.EFile, e error) (out []APIFile, err error) {
+ if e != nil {
+ return nil, e
+ }
+
+ for _, f := range in {
+ g := APIFile{EFile: f}
+
+ var deps []fic.Flag
+ deps, err = f.GetDepends()
+ if err != nil {
+ return
+ }
+
+ for _, d := range deps {
+ if k, ok := d.(*fic.FlagKey); ok {
+ k, err = fic.GetFlagKey(k.Id)
+ if err != nil {
+ return
+ }
+
+ g.Depends = append(g.Depends, k)
+ } else if m, ok := d.(*fic.MCQ); ok {
+ m, err = fic.GetMCQ(m.Id)
+ if err != nil {
+ return
+ }
+
+ g.Depends = append(g.Depends, m)
+ } else {
+ err = fmt.Errorf("Unknown type %T to handle file dependancy", k)
+ return
+ }
+ }
+
+ out = append(out, g)
+ }
+
+ return
+}
+
+func listFiles(c *gin.Context) {
+ var files []APIFile
+ var err error
+
+ if exercice, exists := c.Get("exercice"); exists {
+ files, err = genFileList(exercice.(*fic.Exercice).GetFiles())
+ } else {
+ files, err = genFileList(fic.GetFiles())
+ }
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, files)
+}
+
+func clearFiles(c *gin.Context) {
+ err := os.RemoveAll(fic.FilesDir)
+ if err != nil {
+ log.Println("Unable to remove files:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ err = os.MkdirAll(fic.FilesDir, 0751)
+ if err != nil {
+ log.Println("Unable to create FILES:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ _, err = fic.ClearFiles()
+ if err != nil {
+ log.Println("Unable to clean DB files:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Les fichiers ont bien été effacés. Mais il n'a pas été possible d'effacer la base de données. Refaites une synchronisation maintenant. " + err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, true)
+}
+
+func showFile(c *gin.Context) {
+ c.JSON(http.StatusOK, c.MustGet("file").(*fic.EFile))
+}
type uploadedFile struct {
- URI string
- Digest []byte
- Path string
- Parts []string
+ URI string
+ Digest string
}
-func createExerciceFile(theme fic.Theme, exercice fic.Exercice, args map[string]string, body []byte) (interface{}, error) {
- var uf uploadedFile
- if err := json.Unmarshal(body, &uf); err != nil {
- return nil, err
+func createExerciceFile(c *gin.Context) {
+ exercice, exists := c.Get("exercice")
+ if !exists {
+ c.AbortWithStatusJSON(http.StatusMethodNotAllowed, gin.H{"errmsg": "File can only be added inside an exercice."})
+ return
}
- var hash [sha512.Size]byte
- var logStr string
- var fromURI string
- var getFile func(string) (error)
+ paramsFiles, err := sync.GetExerciceFilesParams(sync.GlobalImporter, exercice.(*fic.Exercice))
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
- if uf.URI != "" {
- hash = sha512.Sum512([]byte(uf.URI))
- logStr = "Import file from Cloud: " + uf.URI + " =>"
- fromURI = uf.URI
- getFile = func(dest string) error { return getCloudFile(uf.URI, dest); }
- } else if uf.Path != "" && len(uf.Parts) > 0 {
- hash = sha512.Sum512([]byte(uf.Path))
- logStr = fmt.Sprintf("Import file from local FS: %s =>", uf.Parts)
- fromURI = uf.Path
- getFile = func(dest string) error {
- if fdto, err := os.Create(dest); err != nil {
- return err
+ var uf uploadedFile
+ err = c.ShouldBindJSON(&uf)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ ret, err := sync.ImportFile(sync.GlobalImporter, uf.URI,
+ func(filePath string, origin string) (interface{}, error) {
+ if digest, err := hex.DecodeString(uf.Digest); err != nil {
+ return nil, err
} else {
- writer := bufio.NewWriter(fdto)
- for _, partname := range uf.Parts {
- if fdfrm, err := os.Open(partname); err != nil {
- return err
- } else {
- reader := bufio.NewReader(fdfrm)
- reader.WriteTo(writer)
- writer.Flush()
- fdfrm.Close()
+ published := true
+ disclaimer := ""
+
+ if f, exists := paramsFiles[filepath.Base(filePath)]; exists {
+ published = !f.Hidden
+
+ if disclaimer, err = sync.ProcessMarkdown(sync.GlobalImporter, f.Disclaimer, exercice.(*fic.Exercice).Path); err != nil {
+ return nil, fmt.Errorf("error during markdown formating of disclaimer: %w", err)
}
}
- fdto.Close()
+
+ return exercice.(*fic.Exercice).ImportFile(filePath, origin, digest, nil, disclaimer, published)
}
- return nil
- }
- } else if uf.Path != "" {
- hash = sha512.Sum512([]byte(uf.Path))
- logStr = "Import file from local FS: " + uf.Path + " =>"
- fromURI = uf.Path
- getFile = func(dest string) error { return os.Symlink(uf.Path, dest); }
- } else {
- return nil, errors.New("URI or path not filled")
+ })
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
}
- pathname := path.Join(fic.FilesDir, strings.ToLower(base32.StdEncoding.EncodeToString(hash[:])), path.Base(fromURI))
-
- if _, err := os.Stat(pathname); os.IsNotExist(err) {
- log.Println(logStr, pathname)
- if err := os.MkdirAll(path.Dir(pathname), 0777); err != nil {
- return nil, err
- } else if err := getFile(pathname); err != nil {
- return nil, err
- }
- }
-
- return exercice.ImportFile(pathname, fromURI)
+ c.JSON(http.StatusOK, ret)
}
-func getCloudFile(pathname string, dest string) error {
- client := http.Client{}
- if req, err := http.NewRequest("GET", CloudDAVBase+pathname, nil); err != nil {
- return err
- } else {
- req.SetBasicAuth(CloudUsername, CloudPassword)
- if resp, err := client.Do(req); err != nil {
- return err
- } else {
- defer resp.Body.Close()
+func updateFile(c *gin.Context) {
+ file := c.MustGet("file").(*fic.EFile)
- if fd, err := os.Create(dest); err != nil {
- return err
- } else {
- defer fd.Close()
-
- if resp.StatusCode != http.StatusOK {
- return errors.New(resp.Status)
- } else {
- writer := bufio.NewWriter(fd)
- reader := bufio.NewReader(resp.Body)
- reader.WriteTo(writer)
- writer.Flush()
- }
- }
- }
+ var uf fic.EFile
+ err := c.ShouldBindJSON(&uf)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
}
- return nil
+
+ uf.Id = file.Id
+
+ if _, err := uf.Update(); err != nil {
+ log.Println("Unable to updateFile:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to update file."})
+ return
+ }
+
+ c.JSON(http.StatusOK, uf)
+}
+
+func deleteFile(c *gin.Context) {
+ file := c.MustGet("file").(*fic.EFile)
+
+ _, err := file.Delete()
+ if err != nil {
+ log.Println("Unable to updateFile:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to update file."})
+ return
+ }
+
+ c.JSON(http.StatusOK, true)
+}
+
+func deleteFileDep(c *gin.Context) {
+ file := c.MustGet("file").(*fic.EFile)
+ depid := c.MustGet("file-depid").(int64)
+
+ err := file.DeleteDepend(&fic.FlagKey{Id: int(depid)})
+ if err != nil {
+ log.Println("Unable to deleteFileDep:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to delete file dependency."})
+ return
+ }
+
+ c.JSON(http.StatusOK, true)
+}
+
+func checkFile(c *gin.Context) {
+ file := c.MustGet("file").(*fic.EFile)
+
+ err := file.CheckFileOnDisk()
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, true)
+}
+
+func gunzipFile(c *gin.Context) {
+ file := c.MustGet("file").(*fic.EFile)
+
+ err := file.GunzipFileOnDisk()
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, true)
}
diff --git a/admin/api/handlers.go b/admin/api/handlers.go
deleted file mode 100644
index 649c5b32..00000000
--- a/admin/api/handlers.go
+++ /dev/null
@@ -1,131 +0,0 @@
-package api
-
-import (
- "encoding/json"
- "fmt"
- "io"
- "log"
- "net/http"
- "strconv"
-
- "srs.epita.fr/fic-server/libfic"
-
- "github.com/gorilla/mux"
-)
-
-
-type DispatchFunction func([]string, []byte) (interface{}, error)
-
-func apiHandler(f func (map[string]string,[]byte) (interface{}, error)) func(http.ResponseWriter, *http.Request) {
- return func(w http.ResponseWriter, r *http.Request) {
- log.Printf("Handling %s request from %s: %s [%s]\n", r.Method, r.RemoteAddr, r.URL.Path, r.UserAgent())
-
- w.Header().Set("Content-Type", "application/json")
-
- var ret interface{}
- var err error = nil
-
- // Read the body
- if r.ContentLength < 0 || r.ContentLength > 6553600 {
- http.Error(w, fmt.Sprintf("{errmsg:\"Request too large or request size unknown\"}", err), http.StatusRequestEntityTooLarge)
- return
- }
- var body []byte
- if r.ContentLength > 0 {
- tmp := make([]byte, 1024)
- for {
- n, err := r.Body.Read(tmp)
- for j := 0; j < n; j++ {
- body = append(body, tmp[j])
- }
- if err != nil || n <= 0 {
- break
- }
- }
- }
-
- ret, err = f(mux.Vars(r), body)
-
- // Format response
- resStatus := http.StatusOK
- if err != nil {
- ret = map[string]string{"errmsg": err.Error()}
- resStatus = http.StatusBadRequest
- log.Println(r.RemoteAddr, resStatus, err.Error())
- }
-
- if ret == nil {
- ret = map[string]string{"errmsg": "Page not found"}
- resStatus = http.StatusNotFound
- }
-
- if str, found := ret.(string); found {
- w.WriteHeader(resStatus)
- io.WriteString(w, str)
- } else if bts, found := ret.([]byte); found {
- w.WriteHeader(resStatus)
- w.Write(bts)
- } else if j, err := json.Marshal(ret); err != nil {
- http.Error(w, fmt.Sprintf("{\"errmsg\":\"%q\"}", err), http.StatusInternalServerError)
- } else {
- w.WriteHeader(resStatus)
- w.Write(j)
- }
- }
-}
-
-func teamHandler(f func(fic.Team,map[string]string,[]byte) (interface{}, error)) func (map[string]string,[]byte) (interface{}, error) {
- return func (args map[string]string, body []byte) (interface{}, error) {
- if tid, err := strconv.Atoi(string(args["tid"])); err != nil {
- if team, err := fic.GetTeamByInitialName(args["tid"]); err != nil {
- return nil, err
- } else {
- return f(team, args, body)
- }
- } else if team, err := fic.GetTeam(tid); err != nil {
- return nil, err
- } else {
- return f(team, args, body)
- }
- }
-}
-
-func themeHandler(f func(fic.Theme,map[string]string,[]byte) (interface{}, error)) func (map[string]string,[]byte) (interface{}, error) {
- return func (args map[string]string, body []byte) (interface{}, error) {
- if tid, err := strconv.Atoi(string(args["tid"])); err != nil {
- return nil, err
- } else if theme, err := fic.GetTheme(tid); err != nil {
- return nil, err
- } else {
- return f(theme, args, body)
- }
- }
-}
-
-func exerciceHandler(f func(fic.Exercice,map[string]string,[]byte) (interface{}, error)) func (map[string]string,[]byte) (interface{}, error) {
- return func (args map[string]string, body []byte) (interface{}, error) {
- if eid, err := strconv.Atoi(string(args["eid"])); err != nil {
- return nil, err
- } else if exercice, err := fic.GetExercice(int64(eid)); err != nil {
- return nil, err
- } else {
- return f(exercice, args, body)
- }
- }
-}
-
-func themedExerciceHandler(f func(fic.Theme,fic.Exercice,map[string]string,[]byte) (interface{}, error)) func (fic.Theme,map[string]string,[]byte) (interface{}, error) {
- return func (theme fic.Theme, args map[string]string, body []byte) (interface{}, error) {
- if eid, err := strconv.Atoi(string(args["eid"])); err != nil {
- return nil, err
- } else if exercice, err := fic.GetExercice(int64(eid)); err != nil {
- return nil, err
- } else {
- return f(theme, exercice, args, body)
- }
- }
-}
-
-func notFound(args map[string]string, body []byte) (interface{}, error) {
- return nil, nil
-}
diff --git a/admin/api/health.go b/admin/api/health.go
new file mode 100644
index 00000000..13d7a0cc
--- /dev/null
+++ b/admin/api/health.go
@@ -0,0 +1,156 @@
+package api
+
+import (
+ "fmt"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "os"
+ "path"
+ "strings"
+ "time"
+
+ "srs.epita.fr/fic-server/admin/pki"
+ "srs.epita.fr/fic-server/libfic"
+
+ "github.com/gin-gonic/gin"
+)
+
+var TimestampCheck = "submissions"
+
+func declareHealthRoutes(router *gin.RouterGroup) {
+ router.GET("/timestamps.json", func(c *gin.Context) {
+ stat, err := os.Stat(TimestampCheck)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": fmt.Sprintf("timestamp.json: %s", err.Error())})
+ return
+ }
+ now := time.Now().UTC()
+ c.JSON(http.StatusOK, gin.H{
+ "frontend": stat.ModTime().UTC(),
+ "backend": now,
+ "diffFB": now.Sub(stat.ModTime()),
+ })
+ })
+ router.GET("/health.json", GetHealth)
+ router.GET("/submissions-stats.json", GetSubmissionsStats)
+ router.GET("/validations-stats.json", GetValidationsStats)
+
+ router.DELETE("/submissions/*path", func(c *gin.Context) {
+ err := os.Remove(path.Join(TimestampCheck, c.Params.ByName("path")))
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": err.Error()})
+ return
+ }
+ c.Status(http.StatusOK)
+ })
+}
+
+type healthFileReport struct {
+ IdTeam string `json:"id_team,omitempty"`
+ Path string `json:"path"`
+ Error string `json:"error"`
+}
+
+func getHealth(pathname string) (ret []healthFileReport) {
+ if ds, err := ioutil.ReadDir(pathname); err != nil {
+ ret = append(ret, healthFileReport{
+ Path: strings.TrimPrefix(pathname, TimestampCheck),
+ Error: fmt.Sprintf("unable to ReadDir: %s", err),
+ })
+ return
+ } else {
+ for _, d := range ds {
+ p := path.Join(pathname, d.Name())
+ if d.IsDir() && d.Name() != ".tmp" && d.Mode()&os.ModeSymlink == 0 {
+ ret = append(ret, getHealth(p)...)
+ } else if !d.IsDir() && d.Mode()&os.ModeSymlink == 0 && time.Since(d.ModTime()) > 2*time.Second {
+ if d.Name() == ".locked" {
+ continue
+ }
+
+ teamDir := strings.TrimPrefix(pathname, TimestampCheck)
+ idteam, _ := pki.GetAssociation(path.Join(TeamsDir, teamDir))
+ ret = append(ret, healthFileReport{
+ IdTeam: idteam,
+ Path: path.Join(teamDir, d.Name()),
+ Error: "existing untreated file",
+ })
+ }
+ }
+ return
+ }
+}
+
+func GetHealth(c *gin.Context) {
+ if _, err := os.Stat(TimestampCheck); err != nil {
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": fmt.Sprintf("health.json: %s", err.Error())})
+ return
+ }
+
+ c.JSON(http.StatusOK, getHealth(TimestampCheck))
+}
+
+type SubmissionsStats struct {
+ NbSubmissionLastMinute uint `json:"nbsubminute"`
+ NbSubmissionLast5Minute uint `json:"nbsub5minute"`
+ NbSubmissionLastQuarter uint `json:"nbsubquarter"`
+ NbSubmissionLastHour uint `json:"nbsubhour"`
+ NbSubmissionLastDay uint `json:"nbsubday"`
+}
+
+func calcSubmissionsStats(tries []time.Time) (stats SubmissionsStats) {
+ lastMinute := time.Now().Add(-1 * time.Minute)
+ last5Minute := time.Now().Add(-5 * time.Minute)
+ lastQuarter := time.Now().Add(-15 * time.Minute)
+ lastHour := time.Now().Add(-1 * time.Hour)
+ lastDay := time.Now().Add(-24 * time.Hour)
+
+ for _, t := range tries {
+ if lastMinute.Before(t) {
+ stats.NbSubmissionLastMinute += 1
+ stats.NbSubmissionLast5Minute += 1
+ stats.NbSubmissionLastQuarter += 1
+ stats.NbSubmissionLastHour += 1
+ stats.NbSubmissionLastDay += 1
+ } else if last5Minute.Before(t) {
+ stats.NbSubmissionLast5Minute += 1
+ stats.NbSubmissionLastQuarter += 1
+ stats.NbSubmissionLastHour += 1
+ stats.NbSubmissionLastDay += 1
+ } else if lastQuarter.Before(t) {
+ stats.NbSubmissionLastQuarter += 1
+ stats.NbSubmissionLastHour += 1
+ stats.NbSubmissionLastDay += 1
+ } else if lastHour.Before(t) {
+ stats.NbSubmissionLastHour += 1
+ stats.NbSubmissionLastDay += 1
+ } else if lastDay.Before(t) {
+ stats.NbSubmissionLastDay += 1
+ }
+ }
+
+ return
+}
+
+func GetSubmissionsStats(c *gin.Context) {
+ tries, err := fic.GetTries(nil, nil)
+ if err != nil {
+ log.Println("Unable to GetTries:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieves tries."})
+ return
+ }
+
+ c.JSON(http.StatusOK, calcSubmissionsStats(tries))
+}
+
+func GetValidationsStats(c *gin.Context) {
+ tries, err := fic.GetValidations(nil, nil)
+ if err != nil {
+ log.Println("Unable to GetTries:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieves tries."})
+ return
+ }
+
+ c.JSON(http.StatusOK, calcSubmissionsStats(tries))
+}
diff --git a/admin/api/monitor.go b/admin/api/monitor.go
new file mode 100644
index 00000000..3d6a9114
--- /dev/null
+++ b/admin/api/monitor.go
@@ -0,0 +1,98 @@
+package api
+
+import (
+ "bufio"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "strconv"
+ "strings"
+
+ "github.com/gin-gonic/gin"
+)
+
+func declareMonitorRoutes(router *gin.RouterGroup) {
+ router.GET("/monitor", func(c *gin.Context) {
+ c.JSON(http.StatusOK, gin.H{
+ "localhost": genLocalConstants(),
+ })
+ })
+}
+
+func readLoadAvg(fd *os.File) (ret map[string]float64) {
+ if s, err := ioutil.ReadAll(fd); err == nil {
+ f := strings.Fields(strings.TrimSpace(string(s)))
+ if len(f) >= 3 {
+ ret = map[string]float64{}
+ ret["1m"], _ = strconv.ParseFloat(f[0], 64)
+ ret["5m"], _ = strconv.ParseFloat(f[1], 64)
+ ret["15m"], _ = strconv.ParseFloat(f[2], 64)
+ }
+ }
+ return
+}
+
+func readMeminfo(fd *os.File) (ret map[string]uint64) {
+ ret = map[string]uint64{}
+
+ scanner := bufio.NewScanner(fd)
+ for scanner.Scan() {
+ f := strings.Fields(strings.TrimSpace(scanner.Text()))
+ if len(f) >= 2 {
+ if v, err := strconv.ParseUint(f[1], 10, 64); err == nil {
+ ret[strings.ToLower(strings.TrimSuffix(f[0], ":"))] = v * 1024
+ }
+ }
+ }
+
+ return
+}
+
+func readCPUStats(fd *os.File) (ret map[string]map[string]uint64) {
+ ret = map[string]map[string]uint64{}
+
+ scanner := bufio.NewScanner(fd)
+ for scanner.Scan() {
+ f := strings.Fields(strings.TrimSpace(scanner.Text()))
+ if len(f[0]) >= 4 && f[0][0:3] == "cpu" && len(f) >= 8 {
+ ret[f[0]] = map[string]uint64{}
+ var total uint64 = 0
+ for i, k := range []string{"user", "nice", "system", "idle", "iowait", "irq", "softirq"} {
+ if v, err := strconv.ParseUint(f[i+1], 10, 64); err == nil {
+ ret[f[0]][k] = v
+ total += v
+ }
+ }
+ ret[f[0]]["total"] = total
+ }
+ }
+
+ return
+}
+
+func genLocalConstants() interface{} {
+ ret := map[string]interface{}{}
+
+ fi, err := os.Open("/proc/loadavg")
+ if err != nil {
+ return err
+ }
+ defer fi.Close()
+ ret["loadavg"] = readLoadAvg(fi)
+
+ fi, err = os.Open("/proc/meminfo")
+ if err != nil {
+ return err
+ }
+ defer fi.Close()
+ ret["meminfo"] = readMeminfo(fi)
+
+ fi, err = os.Open("/proc/stat")
+ if err != nil {
+ return err
+ }
+ defer fi.Close()
+ ret["cpustat"] = readCPUStats(fi)
+
+ return ret
+}
diff --git a/admin/api/password.go b/admin/api/password.go
new file mode 100644
index 00000000..d37153e9
--- /dev/null
+++ b/admin/api/password.go
@@ -0,0 +1,360 @@
+package api
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "os"
+ "path"
+ "text/template"
+ "unicode"
+
+ "srs.epita.fr/fic-server/admin/pki"
+ "srs.epita.fr/fic-server/libfic"
+
+ "github.com/gin-gonic/gin"
+)
+
+var (
+ OidcIssuer = "live.fic.srs.epita.fr"
+ OidcClientId = "epita-challenge"
+ OidcSecret = ""
+)
+
+func declarePasswordRoutes(router *gin.RouterGroup) {
+ router.POST("/password", func(c *gin.Context) {
+ passwd, err := fic.GeneratePassword()
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"password": passwd})
+ })
+ router.GET("/oauth-status", func(c *gin.Context) {
+ c.JSON(http.StatusOK, gin.H{
+ "secret_defined": OidcSecret != "",
+ })
+ })
+ router.GET("/dex.yaml", func(c *gin.Context) {
+ cfg, err := genDexConfig()
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ c.String(http.StatusOK, string(cfg))
+ })
+ router.POST("/dex.yaml", func(c *gin.Context) {
+ if dexcfg, err := genDexConfig(); err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ } else if err := ioutil.WriteFile(path.Join(pki.PKIDir, "shared", "dex-config.yaml"), []byte(dexcfg), 0644); err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, true)
+ })
+ router.GET("/dex-password.tpl", func(c *gin.Context) {
+ passtpl, err := genDexPasswordTpl()
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ c.String(http.StatusOK, string(passtpl))
+ })
+ router.POST("/dex-password.tpl", func(c *gin.Context) {
+ if dexcfg, err := genDexPasswordTpl(); err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ } else if err := ioutil.WriteFile(path.Join(pki.PKIDir, "shared", "dex-password.tpl"), []byte(dexcfg), 0644); err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, true)
+ })
+ router.GET("/vouch-proxy.yaml", func(c *gin.Context) {
+ cfg, err := genVouchProxyConfig()
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ c.String(http.StatusOK, string(cfg))
+ })
+ router.POST("/vouch-proxy.yaml", func(c *gin.Context) {
+ if dexcfg, err := genVouchProxyConfig(); err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ } else if err := ioutil.WriteFile(path.Join(pki.PKIDir, "shared", "vouch-config.yaml"), []byte(dexcfg), 0644); err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, true)
+ })
+}
+
+func declareTeamsPasswordRoutes(router *gin.RouterGroup) {
+ router.GET("/password", func(c *gin.Context) {
+ team := c.MustGet("team").(*fic.Team)
+
+ if team.Password != nil {
+ c.String(http.StatusOK, *team.Password)
+ } else {
+ c.AbortWithStatusJSON(http.StatusNotFound, nil)
+ }
+ })
+ router.POST("/password", func(c *gin.Context) {
+ team := c.MustGet("team").(*fic.Team)
+
+ if passwd, err := fic.GeneratePassword(); err != nil {
+ log.Println("Unable to GeneratePassword:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Something went wrong when generating the new team password"})
+ return
+ } else {
+ team.Password = &passwd
+
+ _, err := team.Update()
+ if err != nil {
+ log.Println("Unable to Update Team:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Something went wrong when updating the new team password"})
+ return
+ }
+
+ c.JSON(http.StatusOK, team)
+ }
+ })
+}
+
+const dexcfgtpl = `issuer: {{ .Issuer }}
+storage:
+ type: sqlite3
+ config:
+ file: /var/dex/dex.db
+web:
+ http: 0.0.0.0:5556
+frontend:
+ issuer: {{ .Name }}
+ logoURL: {{ .LogoPath }}
+ dir: /srv/dex/web/
+oauth2:
+ skipApprovalScreen: true
+staticClients:
+{{ range $c := .Clients }}
+- id: {{ $c.Id }}
+ name: {{ $c.Name }}
+ redirectURIs: [{{ range $u := $c.RedirectURIs }}'{{ $u }}'{{ end }}]
+ secret: {{ $c.Secret }}
+{{ end }}
+enablePasswordDB: true
+staticPasswords:
+{{ range $t := .Teams }}
+ - email: "team{{ printf "%02d" $t.Id }}"
+ hash: "{{with $t }}{{ .HashedPassword }}{{end}}"
+{{ end }}
+`
+
+const dexpasswdtpl = `{{ "{{" }} template "header.html" . {{ "}}" }}
+
+
+
+ Bienvenue au {{ .Name }} !
+
+
+ {{ "{{" }} if .BackLink {{ "}}" }}
+
+ {{ "{{" }} end {{ "}}" }}
+
+
+{{ "{{" }} template "footer.html" . {{ "}}" }}
+`
+
+type dexConfigClient struct {
+ Id string
+ Name string
+ RedirectURIs []string
+ Secret string
+}
+
+type dexConfig struct {
+ Name string
+ Issuer string
+ Clients []dexConfigClient
+ Teams []*fic.Team
+ LogoPath string
+}
+
+func genDexConfig() ([]byte, error) {
+ if OidcSecret == "" {
+ return nil, fmt.Errorf("Unable to generate dex configuration: OIDC Secret not defined. Please define FICOIDC_SECRET in your environment.")
+ }
+
+ teams, err := fic.GetTeams()
+ if err != nil {
+ return nil, err
+ }
+
+ b := bytes.NewBufferString("")
+
+ challengeInfo, err := GetChallengeInfo()
+ if err != nil {
+ return nil, fmt.Errorf("Cannot create template: %w", err)
+ }
+
+ // Lower the first letter to be included in a sentence.
+ name := []rune(challengeInfo.Title)
+ if len(name) > 0 {
+ name[0] = unicode.ToLower(name[0])
+ }
+
+ logoPath := ""
+ if len(challengeInfo.MainLogo) > 0 {
+ logoPath = path.Join("../../files", "logo", path.Base(challengeInfo.MainLogo[len(challengeInfo.MainLogo)-1]))
+ }
+
+ dexTmpl, err := template.New("dexcfg").Parse(dexcfgtpl)
+ if err != nil {
+ return nil, fmt.Errorf("Cannot create template: %w", err)
+ }
+
+ err = dexTmpl.Execute(b, dexConfig{
+ Name: string(name),
+ Issuer: "https://" + OidcIssuer,
+ Clients: []dexConfigClient{
+ dexConfigClient{
+ Id: OidcClientId,
+ Name: challengeInfo.Title,
+ RedirectURIs: []string{"https://" + OidcIssuer + "/challenge_access/auth"},
+ Secret: OidcSecret,
+ },
+ },
+ Teams: teams,
+ LogoPath: logoPath,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("An error occurs during template execution: %w", err)
+ }
+
+ // Also generate team associations
+ for _, team := range teams {
+ if _, err := os.Stat(path.Join(TeamsDir, fmt.Sprintf("team%02d", team.Id))); err == nil {
+ if err = os.Remove(path.Join(TeamsDir, fmt.Sprintf("team%02d", team.Id))); err != nil {
+ log.Println("Unable to remove existing association symlink:", err.Error())
+ return nil, fmt.Errorf("Unable to remove existing association symlink: %s", err.Error())
+ }
+ }
+ if err := os.Symlink(fmt.Sprintf("%d", team.Id), path.Join(TeamsDir, fmt.Sprintf("team%02d", team.Id))); err != nil {
+ log.Println("Unable to create association symlink:", err.Error())
+ return nil, fmt.Errorf("Unable to create association symlink: %s", err.Error())
+ }
+ }
+
+ return b.Bytes(), nil
+}
+
+func genDexPasswordTpl() ([]byte, error) {
+ challengeInfo, err := GetChallengeInfo()
+ if err != nil {
+ return nil, fmt.Errorf("Cannot create template: %w", err)
+ }
+
+ if teams, err := fic.GetTeams(); err != nil {
+ return nil, err
+ } else {
+ b := bytes.NewBufferString("")
+
+ if dexTmpl, err := template.New("dexpasswd").Parse(dexpasswdtpl); err != nil {
+ return nil, fmt.Errorf("Cannot create template: %w", err)
+ } else if err = dexTmpl.Execute(b, dexConfig{
+ Teams: teams,
+ Name: challengeInfo.Title,
+ }); err != nil {
+ return nil, fmt.Errorf("An error occurs during template execution: %w", err)
+ } else {
+ return b.Bytes(), nil
+ }
+ }
+}
+
+const vouchcfgtpl = `# CONFIGURATION FILE HANDLED BY fic-admin
+# DO NOT MODIFY IT BY HAND
+
+vouch:
+ logLevel: debug
+ allowAllUsers: true
+ document_root: /challenge_access
+
+ cookie:
+ domain: {{ .Domain }}
+
+oauth:
+ provider: oidc
+ client_id: {{ .ClientId }}
+ client_secret: {{ .ClientSecret }}
+ callback_urls:
+ - https://{{ .Domain }}/challenge_access/auth
+ auth_url: https://{{ .Domain }}/auth
+ token_url: http://127.0.0.1:5556/token
+ user_info_url: http://127.0.0.1:5556/userinfo
+ scopes:
+ - openid
+ - email
+`
+
+type vouchProxyConfig struct {
+ Domain string
+ ClientId string
+ ClientSecret string
+}
+
+func genVouchProxyConfig() ([]byte, error) {
+ if OidcSecret == "" {
+ return nil, fmt.Errorf("Unable to generate vouch proxy configuration: OIDC Secret not defined. Please define FICOIDC_SECRET in your environment.")
+ }
+
+ b := bytes.NewBufferString("")
+
+ if vouchTmpl, err := template.New("vouchcfg").Parse(vouchcfgtpl); err != nil {
+ return nil, fmt.Errorf("Cannot create template: %w", err)
+ } else if err = vouchTmpl.Execute(b, vouchProxyConfig{
+ Domain: OidcIssuer,
+ ClientId: OidcClientId,
+ ClientSecret: OidcSecret,
+ }); err != nil {
+ return nil, fmt.Errorf("An error occurs during template execution: %w", err)
+ } else {
+ return b.Bytes(), nil
+ }
+}
diff --git a/admin/api/public.go b/admin/api/public.go
new file mode 100644
index 00000000..f2de4ea0
--- /dev/null
+++ b/admin/api/public.go
@@ -0,0 +1,206 @@
+package api
+
+import (
+ "encoding/json"
+ "fmt"
+ "log"
+ "net/http"
+ "os"
+ "path"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/gin-gonic/gin"
+)
+
+var DashboardDir string
+
+func declarePublicRoutes(router *gin.RouterGroup) {
+ router.GET("/public/", listPublic)
+ router.GET("/public/:sid", getPublic)
+ router.DELETE("/public/:sid", deletePublic)
+ router.PUT("/public/:sid", savePublic)
+}
+
+type FICPublicScene struct {
+ Type string `json:"type"`
+ Params map[string]interface{} `json:"params"`
+}
+
+type FICPublicDisplay struct {
+ Scenes []FICPublicScene `json:"scenes"`
+ Side []FICPublicScene `json:"side"`
+ CustomCountdown map[string]interface{} `json:"customCountdown"`
+ HideEvents bool `json:"hideEvents"`
+ HideCountdown bool `json:"hideCountdown"`
+ HideCarousel bool `json:"hideCarousel"`
+ PropagationTime *time.Time `json:"propagationTime,omitempty"`
+}
+
+func InitDashboardPresets(dir string) error {
+ return nil
+}
+
+func readPublic(path string) (FICPublicDisplay, error) {
+ var s FICPublicDisplay
+ if fd, err := os.Open(path); err != nil {
+ return s, err
+ } else {
+ defer fd.Close()
+ jdec := json.NewDecoder(fd)
+
+ if err := jdec.Decode(&s); err != nil {
+ return s, err
+ }
+
+ return s, nil
+ }
+}
+
+func savePublicTo(path string, s FICPublicDisplay) error {
+ if fd, err := os.Create(path); err != nil {
+ return err
+ } else {
+ defer fd.Close()
+ jenc := json.NewEncoder(fd)
+
+ if err := jenc.Encode(s); err != nil {
+ return err
+ }
+
+ return nil
+ }
+}
+
+type DashboardFiles struct {
+ Presets []string `json:"presets"`
+ Nexts []*NextDashboardFile `json:"nexts"`
+}
+
+type NextDashboardFile struct {
+ Name string `json:"name"`
+ Screen int `json:"screen"`
+ Date time.Time `json:"date"`
+}
+
+func listPublic(c *gin.Context) {
+ files, err := os.ReadDir(DashboardDir)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ var ret DashboardFiles
+ for _, file := range files {
+ if strings.HasPrefix(file.Name(), "preset-") {
+ ret.Presets = append(ret.Presets, strings.TrimSuffix(strings.TrimPrefix(file.Name(), "preset-"), ".json"))
+ continue
+ }
+
+ if !strings.HasPrefix(file.Name(), "public") || len(file.Name()) < 18 {
+ continue
+ }
+
+ ts, err := strconv.ParseInt(file.Name()[8:18], 10, 64)
+ if err == nil {
+ s, _ := strconv.Atoi(file.Name()[6:7])
+ ret.Nexts = append(ret.Nexts, &NextDashboardFile{
+ Name: file.Name()[6:18],
+ Screen: s,
+ Date: time.Unix(ts, 0),
+ })
+ }
+ }
+
+ c.JSON(http.StatusOK, ret)
+}
+
+func getPublic(c *gin.Context) {
+ if strings.Contains(c.Params.ByName("sid"), "/") {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "sid cannot contains /"})
+ return
+ }
+
+ filename := fmt.Sprintf("public%s.json", c.Params.ByName("sid"))
+ if strings.HasPrefix(c.Params.ByName("sid"), "preset-") {
+ filename = fmt.Sprintf("%s.json", c.Params.ByName("sid"))
+ }
+
+ if _, err := os.Stat(path.Join(DashboardDir, filename)); !os.IsNotExist(err) {
+ p, err := readPublic(path.Join(DashboardDir, filename))
+ if err != nil {
+ log.Println("Unable to readPublic in getPublic:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during scene retrieval."})
+ return
+ }
+
+ c.JSON(http.StatusOK, p)
+ return
+ }
+
+ c.JSON(http.StatusOK, FICPublicDisplay{Scenes: []FICPublicScene{}, Side: []FICPublicScene{}})
+}
+
+func deletePublic(c *gin.Context) {
+ if strings.Contains(c.Params.ByName("sid"), "/") {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "sid cannot contains /"})
+ return
+ }
+
+ filename := fmt.Sprintf("public%s.json", c.Params.ByName("sid"))
+ if strings.HasPrefix(c.Params.ByName("sid"), "preset-") {
+ filename = fmt.Sprintf("%s.json", c.Params.ByName("sid"))
+ }
+
+ if len(filename) == 12 {
+ if err := savePublicTo(path.Join(DashboardDir, filename), FICPublicDisplay{}); err != nil {
+ log.Println("Unable to deletePublic:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during scene deletion."})
+ return
+ }
+ } else {
+ if err := os.Remove(path.Join(DashboardDir, filename)); err != nil {
+ log.Println("Unable to deletePublic:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during scene deletion."})
+ return
+ }
+ }
+
+ c.JSON(http.StatusOK, FICPublicDisplay{Scenes: []FICPublicScene{}, Side: []FICPublicScene{}})
+}
+
+func savePublic(c *gin.Context) {
+ if strings.Contains(c.Params.ByName("sid"), "/") {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "sid cannot contains /"})
+ return
+ }
+
+ var scenes FICPublicDisplay
+ err := c.ShouldBindJSON(&scenes)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ filename := fmt.Sprintf("public%s.json", c.Params.ByName("sid"))
+ if c.Request.URL.Query().Has("t") {
+ t, err := time.Parse(time.RFC3339, c.Request.URL.Query().Get("t"))
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ filename = fmt.Sprintf("public%s-%d.json", c.Params.ByName("sid"), t.Unix())
+ } else if c.Request.URL.Query().Has("p") {
+ filename = fmt.Sprintf("preset-%s.json", c.Request.URL.Query().Get("p"))
+ }
+
+ if err := savePublicTo(path.Join(DashboardDir, filename), scenes); err != nil {
+ log.Println("Unable to savePublicTo:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during scene saving."})
+ return
+ }
+
+ c.JSON(http.StatusOK, scenes)
+}
diff --git a/admin/api/qa.go b/admin/api/qa.go
new file mode 100644
index 00000000..10bc02c4
--- /dev/null
+++ b/admin/api/qa.go
@@ -0,0 +1,119 @@
+package api
+
+import (
+ "log"
+ "net/http"
+ "strconv"
+
+ "srs.epita.fr/fic-server/libfic"
+
+ "github.com/gin-gonic/gin"
+)
+
+func declareQARoutes(router *gin.RouterGroup) {
+ router.POST("/qa/", importExerciceQA)
+
+ apiQARoutes := router.Group("/qa/:qid")
+ apiQARoutes.Use(QAHandler)
+ apiQARoutes.POST("/comments", importQAComment)
+}
+
+func QAHandler(c *gin.Context) {
+ qid, err := strconv.ParseInt(string(c.Params.ByName("qid")), 10, 64)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid QA identifier"})
+ return
+ }
+
+ qa, err := fic.GetQAQuery(qid)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "QA query not found"})
+ return
+ }
+
+ c.Set("qa-query", qa)
+
+ c.Next()
+}
+
+func importExerciceQA(c *gin.Context) {
+ // Create a new query
+ var uq fic.QAQuery
+ err := c.ShouldBindJSON(&uq)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ var exercice *fic.Exercice
+ if uq.IdExercice == 0 {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "id_exercice not filled"})
+ return
+ } else if exercice, err = fic.GetExercice(uq.IdExercice); err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Unable to find requested exercice"})
+ return
+ }
+
+ if len(uq.State) == 0 {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "State not filled"})
+ return
+ }
+
+ if len(uq.Subject) == 0 {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Subject not filled"})
+ return
+ }
+
+ if qa, err := exercice.NewQAQuery(uq.Subject, uq.IdTeam, uq.User, uq.State, nil); err != nil {
+ log.Println("Unable to importExerciceQA:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during query creation."})
+ return
+ } else {
+ qa.Creation = uq.Creation
+ qa.Solved = uq.Solved
+ qa.Closed = uq.Closed
+
+ _, err = qa.Update()
+ if err != nil {
+ log.Println("Unable to update in importExerciceQA:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during query updating."})
+ return
+ }
+
+ c.JSON(http.StatusOK, qa)
+ }
+}
+
+func importQAComment(c *gin.Context) {
+ query := c.MustGet("qa-query").(*fic.QAQuery)
+
+ // Create a new query
+ var uc fic.QAComment
+ err := c.ShouldBindJSON(&uc)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ if len(uc.Content) == 0 {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Empty comment"})
+ return
+ }
+
+ if qac, err := query.AddComment(uc.Content, uc.IdTeam, uc.User); err != nil {
+ log.Println("Unable to AddComment in importQAComment:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during comment creation."})
+ return
+ } else {
+ qac.Date = uc.Date
+
+ _, err = qac.Update()
+ if err != nil {
+ log.Println("Unable to Update comment in importQAComment")
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during comment creation."})
+ return
+ }
+
+ c.JSON(http.StatusOK, qac)
+ }
+}
diff --git a/admin/api/repositories.go b/admin/api/repositories.go
new file mode 100644
index 00000000..9afaea5d
--- /dev/null
+++ b/admin/api/repositories.go
@@ -0,0 +1,67 @@
+package api
+
+import (
+ "net/http"
+ "strings"
+
+ "srs.epita.fr/fic-server/admin/sync"
+
+ "github.com/gin-gonic/gin"
+)
+
+func declareRepositoriesRoutes(router *gin.RouterGroup) {
+ if gi, ok := sync.GlobalImporter.(sync.GitImporter); ok {
+ router.GET("/repositories", func(c *gin.Context) {
+ mod, err := gi.GetSubmodules()
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{"repositories": mod})
+ })
+
+ router.GET("/repositories/*repopath", func(c *gin.Context) {
+ repopath := strings.TrimPrefix(c.Param("repopath"), "/")
+
+ mod, err := gi.GetSubmodule(repopath)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+ c.JSON(http.StatusOK, mod)
+ })
+
+ router.POST("/repositories/*repopath", func(c *gin.Context) {
+ repopath := strings.TrimPrefix(c.Param("repopath"), "/")
+
+ mod, err := gi.IsRepositoryUptodate(repopath)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+ c.JSON(http.StatusOK, mod)
+ })
+
+ router.DELETE("/repositories/*repopath", func(c *gin.Context) {
+ di, ok := sync.GlobalImporter.(sync.DeletableImporter)
+ if !ok {
+ c.AbortWithStatusJSON(http.StatusNotImplemented, gin.H{"errmsg": "Not implemented"})
+ return
+ }
+
+ if strings.Contains(c.Param("repopath"), "..") {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Repopath contains invalid characters"})
+ return
+ }
+
+ repopath := strings.TrimPrefix(c.Param("repopath"), "/")
+
+ err := di.DeleteDir(repopath)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+ c.JSON(http.StatusOK, true)
+ })
+ }
+}
diff --git a/admin/api/router.go b/admin/api/router.go
index 3c6be0ec..743bc22e 100644
--- a/admin/api/router.go
+++ b/admin/api/router.go
@@ -1,14 +1,29 @@
package api
import (
- "github.com/gorilla/mux"
+ "github.com/gin-gonic/gin"
)
-var api_router = mux.NewRouter().StrictSlash(true)
+func DeclareRoutes(router *gin.RouterGroup) {
+ apiRoutes := router.Group("/api")
-var router = api_router.PathPrefix("/api/").Subrouter()
-
-
-func Router() *mux.Router {
- return api_router
+ declareCertificateRoutes(apiRoutes)
+ declareClaimsRoutes(apiRoutes)
+ declareEventsRoutes(apiRoutes)
+ declareExercicesRoutes(apiRoutes)
+ declareExportRoutes(apiRoutes)
+ declareFilesGlobalRoutes(apiRoutes)
+ declareFilesRoutes(apiRoutes)
+ declareGlobalExercicesRoutes(apiRoutes)
+ declareHealthRoutes(apiRoutes)
+ declareMonitorRoutes(apiRoutes)
+ declarePasswordRoutes(apiRoutes)
+ declarePublicRoutes(apiRoutes)
+ declareQARoutes(apiRoutes)
+ declareRepositoriesRoutes(apiRoutes)
+ declareTeamsRoutes(apiRoutes)
+ declareThemesRoutes(apiRoutes)
+ declareSettingsRoutes(apiRoutes)
+ declareSyncRoutes(apiRoutes)
+ DeclareVersionRoutes(apiRoutes)
}
diff --git a/admin/api/settings.go b/admin/api/settings.go
new file mode 100644
index 00000000..c16d698d
--- /dev/null
+++ b/admin/api/settings.go
@@ -0,0 +1,425 @@
+package api
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "os"
+ "path"
+ "strconv"
+ "time"
+
+ "srs.epita.fr/fic-server/admin/generation"
+ "srs.epita.fr/fic-server/admin/sync"
+ "srs.epita.fr/fic-server/libfic"
+ "srs.epita.fr/fic-server/settings"
+
+ "github.com/gin-gonic/gin"
+)
+
+var IsProductionEnv = false
+
+func declareSettingsRoutes(router *gin.RouterGroup) {
+ router.GET("/challenge.json", getChallengeInfo)
+ router.PUT("/challenge.json", saveChallengeInfo)
+
+ router.GET("/settings.json", getSettings)
+ router.PUT("/settings.json", saveSettings)
+ router.DELETE("/settings.json", func(c *gin.Context) {
+ err := ResetSettings()
+ if err != nil {
+ log.Println("Unable to ResetSettings:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during setting reset."})
+ return
+ }
+
+ c.JSON(http.StatusOK, true)
+ })
+
+ router.GET("/settings-next", listNextSettings)
+
+ apiNextSettingsRoutes := router.Group("/settings-next/:ts")
+ apiNextSettingsRoutes.Use(NextSettingsHandler)
+ apiNextSettingsRoutes.GET("", getNextSettings)
+ apiNextSettingsRoutes.DELETE("", deleteNextSettings)
+
+ router.POST("/reset", reset)
+ router.POST("/full-generation", fullGeneration)
+
+ router.GET("/prod", func(c *gin.Context) {
+ c.JSON(http.StatusOK, IsProductionEnv)
+ })
+ router.PUT("/prod", func(c *gin.Context) {
+ err := c.ShouldBindJSON(&IsProductionEnv)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, IsProductionEnv)
+ })
+}
+
+func NextSettingsHandler(c *gin.Context) {
+ ts, err := strconv.ParseInt(string(c.Params.ByName("ts")), 10, 64)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid next settings identifier"})
+ return
+ }
+
+ nsf, err := settings.ReadNextSettingsFile(path.Join(settings.SettingsDir, fmt.Sprintf("%d.json", ts)), ts)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Next settings not found"})
+ return
+ }
+
+ c.Set("next-settings", nsf)
+
+ c.Next()
+}
+
+func fullGeneration(c *gin.Context) {
+ resp, err := generation.FullGeneration()
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
+ "errmsg": err.Error(),
+ })
+ return
+ }
+ defer resp.Body.Close()
+
+ v, _ := io.ReadAll(resp.Body)
+ c.JSON(resp.StatusCode, gin.H{
+ "errmsg": string(v),
+ })
+}
+
+func GetChallengeInfo() (*settings.ChallengeInfo, error) {
+ var challengeinfo string
+ var err error
+ if sync.GlobalImporter == nil {
+ if fd, err := os.Open(path.Join(settings.SettingsDir, settings.ChallengeFile)); err == nil {
+ defer fd.Close()
+ var buf []byte
+ buf, err = io.ReadAll(fd)
+ if err == nil {
+ challengeinfo = string(buf)
+ }
+ }
+ } else {
+ challengeinfo, err = sync.GetFileContent(sync.GlobalImporter, settings.ChallengeFile)
+ }
+
+ if err != nil {
+ log.Println("Unable to retrieve challenge.json:", err.Error())
+ return nil, fmt.Errorf("Unable to retrive challenge.json: %w", err)
+ }
+
+ s, err := settings.ReadChallengeInfo(challengeinfo)
+ if err != nil {
+ log.Println("Unable to ReadChallengeInfo:", err.Error())
+ return nil, fmt.Errorf("Unable to read challenge info: %w", err)
+ }
+
+ return s, nil
+}
+
+func getChallengeInfo(c *gin.Context) {
+ if s, err := GetChallengeInfo(); err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ } else {
+ c.JSON(http.StatusOK, s)
+ }
+}
+
+func saveChallengeInfo(c *gin.Context) {
+ var info *settings.ChallengeInfo
+ err := c.ShouldBindJSON(&info)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ if sync.GlobalImporter != nil {
+ jenc, err := json.Marshal(info)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ err = sync.WriteFileContent(sync.GlobalImporter, "challenge.json", jenc)
+ if err != nil {
+ log.Println("Unable to SaveChallengeInfo:", err.Error())
+ // Ignore the error, try to continue
+ }
+
+ err = sync.ImportChallengeInfo(info, DashboardDir)
+ if err != nil {
+ log.Println("Unable to ImportChallengeInfo:", err.Error())
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Something goes wrong when trying to import related files: %s", err.Error())})
+ return
+ }
+ }
+
+ if err := settings.SaveChallengeInfo(path.Join(settings.SettingsDir, settings.ChallengeFile), info); err != nil {
+ log.Println("Unable to SaveChallengeInfo:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to save distributed challenge info: %s", err.Error())})
+ return
+ }
+
+ c.JSON(http.StatusOK, info)
+}
+
+func getSettings(c *gin.Context) {
+ s, err := settings.ReadSettings(path.Join(settings.SettingsDir, settings.SettingsFile))
+ if err != nil {
+ log.Println("Unable to ReadSettings:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to read settings: %s", err.Error())})
+ return
+ }
+
+ s.WorkInProgress = !IsProductionEnv
+ c.Writer.Header().Add("X-FIC-Time", fmt.Sprintf("%d", time.Now().Unix()))
+ c.JSON(http.StatusOK, s)
+}
+
+func saveSettings(c *gin.Context) {
+ var config *settings.Settings
+ err := c.ShouldBindJSON(&config)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ // Is this a future setting?
+ if c.Request.URL.Query().Has("t") {
+ t, err := time.Parse(time.RFC3339, c.Request.URL.Query().Get("t"))
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ // Load current settings to perform diff later
+ init_settings, err := settings.ReadSettings(path.Join(settings.SettingsDir, settings.SettingsFile))
+ if err != nil {
+ log.Println("Unable to ReadSettings:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to read settings: %s", err.Error())})
+ return
+ }
+
+ current_settings := init_settings
+ // Apply already registered settings
+ nsu, err := settings.MergeNextSettingsUntil(&t)
+ if err == nil {
+ current_settings = settings.MergeSettings(*init_settings, nsu)
+ } else {
+ log.Println("Unable to MergeNextSettingsUntil:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to merge next settings: %s", err.Error())})
+ return
+ }
+
+ // Keep only diff
+ diff := settings.DiffSettings(current_settings, config)
+
+ hasItems := false
+ for _, _ = range diff {
+ hasItems = true
+ break
+ }
+
+ if !hasItems {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "No difference to apply."})
+ return
+ }
+
+ if !c.Request.URL.Query().Has("erase") {
+ // Check if there is already diff to apply at the given time
+ if nsf, err := settings.ReadNextSettingsFile(path.Join(settings.SettingsDir, fmt.Sprintf("%d.json", t.Unix())), t.Unix()); err == nil {
+ for k, v := range nsf.Values {
+ if _, ok := diff[k]; !ok {
+ diff[k] = v
+ }
+ }
+ }
+ }
+
+ // Save the diff
+ settings.SaveSettings(path.Join(settings.SettingsDir, fmt.Sprintf("%d.json", t.Unix())), diff)
+
+ // Return current settings
+ c.JSON(http.StatusOK, current_settings)
+ } else {
+ // Just apply settings right now!
+ if err := settings.SaveSettings(path.Join(settings.SettingsDir, settings.SettingsFile), config); err != nil {
+ log.Println("Unable to SaveSettings:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to save settings: %s", err.Error())})
+ return
+ }
+
+ ApplySettings(config)
+ c.JSON(http.StatusOK, config)
+ }
+}
+
+func listNextSettings(c *gin.Context) {
+ nsf, err := settings.ListNextSettingsFiles()
+ if err != nil {
+ log.Println("Unable to ListNextSettingsFiles:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to list next settings files: %s", err.Error())})
+ return
+ }
+
+ c.JSON(http.StatusOK, nsf)
+}
+
+func getNextSettings(c *gin.Context) {
+ c.JSON(http.StatusOK, c.MustGet("next-settings").(*settings.NextSettingsFile))
+}
+
+func deleteNextSettings(c *gin.Context) {
+ nsf := c.MustGet("next-settings").(*settings.NextSettingsFile)
+
+ err := os.Remove(path.Join(settings.SettingsDir, fmt.Sprintf("%d.json", nsf.Id)))
+ if err != nil {
+ log.Println("Unable to remove the file:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to remove the file: %s", err.Error())})
+ return
+ }
+
+ c.JSON(http.StatusOK, true)
+}
+
+func ApplySettings(config *settings.Settings) {
+ fic.PartialValidation = config.PartialValidation
+ fic.UnlockedChallengeDepth = config.UnlockedChallengeDepth
+ fic.UnlockedChallengeUpTo = config.UnlockedChallengeUpTo
+ fic.DisplayAllFlags = config.DisplayAllFlags
+ fic.HideCaseSensitivity = config.HideCaseSensitivity
+ fic.UnlockedStandaloneExercices = config.UnlockedStandaloneExercices
+ fic.UnlockedStandaloneExercicesByThemeStepValidation = config.UnlockedStandaloneExercicesByThemeStepValidation
+ fic.UnlockedStandaloneExercicesByStandaloneExerciceValidation = config.UnlockedStandaloneExercicesByStandaloneExerciceValidation
+ fic.DisplayMCQBadCount = config.DisplayMCQBadCount
+ fic.FirstBlood = config.FirstBlood
+ fic.SubmissionCostBase = config.SubmissionCostBase
+ fic.HintCoefficient = config.HintCurCoefficient
+ fic.WChoiceCoefficient = config.WChoiceCurCoefficient
+ fic.ExerciceCurrentCoefficient = config.ExerciceCurCoefficient
+ fic.GlobalScoreCoefficient = config.GlobalScoreCoefficient
+ fic.SubmissionCostBase = config.SubmissionCostBase
+ fic.SubmissionUniqueness = config.SubmissionUniqueness
+ fic.CountOnlyNotGoodTries = config.CountOnlyNotGoodTries
+ fic.QuestionGainRatio = config.QuestionGainRatio
+
+ if config.DiscountedFactor != fic.DiscountedFactor {
+ fic.DiscountedFactor = config.DiscountedFactor
+ if err := fic.DBRecreateDiscountedView(); err != nil {
+ log.Println("Unable to recreate exercices_discounted view:", err.Error())
+ }
+ }
+}
+
+func ResetSettings() error {
+ return settings.SaveSettings(path.Join(settings.SettingsDir, settings.SettingsFile), &settings.Settings{
+ WorkInProgress: IsProductionEnv,
+ FirstBlood: fic.FirstBlood,
+ SubmissionCostBase: fic.SubmissionCostBase,
+ ExerciceCurCoefficient: 1,
+ HintCurCoefficient: 1,
+ WChoiceCurCoefficient: 1,
+ GlobalScoreCoefficient: 1,
+ DiscountedFactor: 0,
+ QuestionGainRatio: 0,
+ UnlockedStandaloneExercices: 10,
+ UnlockedStandaloneExercicesByThemeStepValidation: 1,
+ UnlockedStandaloneExercicesByStandaloneExerciceValidation: 0,
+ AllowRegistration: false,
+ CanJoinTeam: false,
+ DenyTeamCreation: false,
+ DenyNameChange: false,
+ AcceptNewIssue: true,
+ QAenabled: false,
+ EnableResolutionRoute: false,
+ PartialValidation: true,
+ UnlockedChallengeDepth: 0,
+ SubmissionUniqueness: true,
+ CountOnlyNotGoodTries: true,
+ DisplayAllFlags: false,
+ DisplayMCQBadCount: false,
+ EventKindness: false,
+ })
+}
+
+func ResetChallengeInfo() error {
+ return settings.SaveChallengeInfo(path.Join(settings.SettingsDir, settings.ChallengeFile), &settings.ChallengeInfo{
+ Title: "Challenge forensic",
+ SubTitle: "sous le patronage du commandement de la cyberdéfense",
+ Authors: "Laboratoire SRS, ÉPITA",
+ VideosLink: "",
+ Description: `Le challenge forensic vous place dans la peau de spécialistes en investigation numérique . Nous mettons à votre disposition une vingtaine de scénarios différents , dans lesquels vous devrez faire les différentes étapes de la caractérisation d’une réponse à incident proposées.
+Chaque scénario met en scène un contexte d’entreprise , ayant découvert récemment qu’elle a été victime d’une cyberattaque . Elle vous demande alors de l’aider à caractériser , afin de mieux comprendre la situation , notamment le mode opératoire de l’adversaire , les impacts de la cyberattaque, le périmètre technique compromis , etc. Il faudra parfois aussi l’éclairer sur les premières étapes de la réaction.
`,
+ Rules: `Déroulement
+Pendant toute la durée du challenge, vous aurez accès à tous les scénarios , mais seulement à la première des 5 étapes. Chaque étape supplémentaire est débloquée lorsque vous validez l’intégralité de l’étape précédente . Toutefois, pour dynamiser le challenge toutes les étapes et tous les scénarios seront débloquées pour la dernière heure du challenge.
+Nous mettons à votre disposition une plateforme sur laquelle vous pourrez obtenir les informations sur le contexte de l’entreprise et, généralement, une série de fichiers qui semblent appropriés pour avancer dans l’investigation.
+La validation d’une étape se fait sur la plateforme, après avoir analysé les informations fournies, en répondant à des questions plus ou moins précises. Il s’agit le plus souvent des mots-clefs que l’on placerait dans un rapport .
+Pour vous débloquer ou accélérer votre investigation, vous pouvez accéder à quelques indices , en échange d’une décote sur votre score d’un certain nombre de points préalablement affichés.
+Calcul des points, bonus, malus et classement
+Chaque équipe dispose d’un compteur de points dans l’intervalle ]-∞;+∞[ (aux détails techniques près), à partir duquel le classement est établi .
+Vous perdez des points en dévoilant des indices , en demandant des propositions de réponses en remplacement de certains champs de texte, ou en essayant un trop grand nombre de fois une réponse .
+Le nombre de points que vous fait perdre un indice dépend habituellement de l’aide qu’il vous apportera et est indiqué avant de le dévoiler, car il peut fluctuer en fonction de l’avancement du challenge.
+Pour chaque champ de texte, vous disposez de 10 tentatives avant de perdre des points (vous perdez les points même si vous ne validez pas l’étape) pour chaque tentative supplémentaire : -0,25 point entre 11 et 20, -0,5 entre 21 et 30, -0,75 entre 31 et 40, …
+La seule manière de gagner des points est de valider une étape d’un scénario dans son intégralité . Le nombre de points gagnés dépend de la difficulté théorique de l’étape ainsi que d’éventuels bonus . Un bonus de 10 % est accordé à la première équipe qui valide une étape. D’autres bonus peuvent ponctuer le challenge, détaillé dans la partie suivante.
+Le classement est établi par équipe, selon le nombre de points récoltés et perdus par tous les membres. En cas d’égalité au score, les équipes sont départagées en fonction de leur ordre d’arrivée à ce score.
+Temps forts
+Le challenge forensic est jalonné de plusieurs temps forts durant lesquels certains calculs détaillés dans la partie précédente peuvent être altérés . L’équipe d’animation du challenge vous avertira environ 15 minutes avant le début de la modification.
+Chaque modification se répercute instantanément dans votre interface, attendez simplement qu’elle apparaisse afin d’être certain d’en bénéficier. Un compte à rebours est généralement affiché sur les écrans pour indiquer la fin d’un temps fort. La fin d’application d’un bonus est déterminé par l’heure d’arrivée de votre demande sur nos serveurs.
+Sans y être limité ou assuré, sachez que durant les précédentes éditions du challenge forensic , nous avons par exemple : doublé les points de défis peu tentés, doublé les points de tous les défis pendant 30 minutes, réduit le coût des indices pendant 15 minutes, etc.
+
+Tous les étudiants de la majeure Système, Réseaux et Sécurité de l’ÉPITA, son équipe enseignante ainsi que le commandement de la cyberdéfense vous souhaitent bon courage pour cette nouvelle éditions du challenge !
`,
+ YourMission: `Bienvenue au challenge forensic !
+Vous voici aujourd'hui dans la peau de spécialistes en investigation numérique . Vous avez à votre disposition une vingtaine de scénarios différents dans lesquels vous devrez faire les différentes étapes de la caractérisation d’une réponse à incident .
+Chaque scénario est découpé en 5 grandes étapes de difficulté croissante . Un certain nombre de points est attribué à chaque étape, avec un processus de validation automatique.
+Un classement est établi en temps réel, tenant compte des différents bonus, en fonction du nombre de points de chaque équipe.
`,
+ })
+}
+
+func reset(c *gin.Context) {
+ var m map[string]string
+ err := c.ShouldBindJSON(&m)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ t, ok := m["type"]
+ if !ok {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Field type not found"})
+ }
+
+ switch t {
+ case "teams":
+ err = fic.ResetTeams()
+ case "challenges":
+ err = fic.ResetExercices()
+ case "game":
+ err = fic.ResetGame()
+ case "annexes":
+ err = fic.ResetAnnexes()
+ case "settings":
+ err = ResetSettings()
+ case "challengeInfo":
+ err = ResetChallengeInfo()
+ default:
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Unknown reset type"})
+ return
+ }
+
+ if err != nil {
+ log.Printf("Unable to reset (type=%q): %s", t, err)
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to performe the reset: %s", err.Error())})
+ return
+ }
+
+ c.JSON(http.StatusOK, true)
+}
diff --git a/admin/api/stats.go b/admin/api/stats.go
deleted file mode 100644
index 85a4b5b6..00000000
--- a/admin/api/stats.go
+++ /dev/null
@@ -1,43 +0,0 @@
-package api
-
-import (
- "fmt"
-
- "srs.epita.fr/fic-server/libfic"
-)
-
-type statsTheme struct {
- SolvedByLevel []int `json:"solvedByLevel"`
-}
-
-type stats struct {
- Themes map[string]statsTheme `json:"themes"`
- TryRank []int64 `json:"tryRank"`
-}
-
-func genStats() (interface{}, error) {
- ret := map[string]statsTheme{}
-
- if themes, err := fic.GetThemes(); err != nil {
- return nil, err
- } else {
- for _, theme := range themes {
- if exercices, err := theme.GetExercices(); err != nil {
- return nil, err
- } else {
- exos := map[string]fic.ExportedExercice{}
- for _, exercice := range exercices {
- exos[fmt.Sprintf("%d", exercice.Id)] = fic.ExportedExercice{
- exercice.Title,
- exercice.Gain,
- exercice.SolvedCount(),
- exercice.TriedTeamCount(),
- }
- }
- ret[fmt.Sprintf("%d", theme.Id)] = statsTheme{}
- }
- }
-
- return ret, nil
- }
-}
diff --git a/admin/api/sync.go b/admin/api/sync.go
new file mode 100644
index 00000000..582c55e0
--- /dev/null
+++ b/admin/api/sync.go
@@ -0,0 +1,411 @@
+package api
+
+import (
+ "fmt"
+ "log"
+ "net/http"
+ "net/url"
+ "os"
+ "path"
+ "reflect"
+ "strings"
+
+ "srs.epita.fr/fic-server/admin/generation"
+ "srs.epita.fr/fic-server/admin/sync"
+ "srs.epita.fr/fic-server/libfic"
+
+ "github.com/gin-gonic/gin"
+ "go.uber.org/multierr"
+)
+
+var lastSyncError = ""
+
+func flatifySyncErrors(errs error) (ret []string) {
+ for _, err := range multierr.Errors(errs) {
+ ret = append(ret, err.Error())
+ }
+ return
+}
+
+func declareSyncRoutes(router *gin.RouterGroup) {
+ apiSyncRoutes := router.Group("/sync")
+
+ // Return the global sync status
+ apiSyncRoutes.GET("/status", func(c *gin.Context) {
+ syncMtd := "Disabled"
+ if sync.GlobalImporter != nil {
+ syncMtd = sync.GlobalImporter.Kind()
+ }
+
+ var syncId *string
+ if sync.GlobalImporter != nil {
+ syncId = sync.GlobalImporter.Id()
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "sync-type": reflect.TypeOf(sync.GlobalImporter).Name(),
+ "sync-id": syncId,
+ "sync": syncMtd,
+ "pullMutex": !sync.OneGitPullStatus(),
+ "syncMutex": !sync.OneDeepSyncStatus() && !sync.OneThemeDeepSyncStatus(),
+ "progress": sync.DeepSyncProgress,
+ "lastError": lastSyncError,
+ })
+ })
+
+ // Base sync checks if the local directory is in sync with remote one.
+ apiSyncRoutes.POST("/base", func(c *gin.Context) {
+ err := sync.GlobalImporter.Sync()
+ if err != nil {
+ lastSyncError = err.Error()
+ c.JSON(http.StatusExpectationFailed, gin.H{"errmsg": err.Error()})
+ } else {
+ lastSyncError = ""
+ c.JSON(http.StatusOK, true)
+ }
+ })
+
+ // Speedy sync performs a recursive synchronization without importing files.
+ apiSyncRoutes.POST("/speed", func(c *gin.Context) {
+ st := sync.SpeedySyncDeep(sync.GlobalImporter)
+ sync.EditDeepReport(&st, false)
+ c.JSON(http.StatusOK, st)
+ })
+
+ // Deep sync: a fully recursive synchronization (can be limited by theme).
+ apiSyncRoutes.POST("/deep", func(c *gin.Context) {
+ r := sync.SyncDeep(sync.GlobalImporter)
+ lastSyncError = ""
+ c.JSON(http.StatusOK, r)
+ })
+
+ apiSyncRoutes.POST("/local-diff", APIDiffDBWithRemote)
+
+ apiSyncDeepRoutes := apiSyncRoutes.Group("/deep/:thid")
+ apiSyncDeepRoutes.Use(ThemeHandler)
+ // Special route to handle standalone exercices
+ apiSyncRoutes.POST("/deep/0", func(c *gin.Context) {
+ var st []string
+ for _, se := range multierr.Errors(sync.SyncThemeDeep(sync.GlobalImporter, &fic.StandaloneExercicesTheme, 0, 250, nil)) {
+ st = append(st, se.Error())
+ }
+ sync.EditDeepReport(&sync.SyncReport{Exercices: st}, false)
+ sync.DeepSyncProgress = 255
+ lastSyncError = ""
+ c.JSON(http.StatusOK, st)
+ })
+ apiSyncDeepRoutes.POST("", func(c *gin.Context) {
+ theme := c.MustGet("theme").(*fic.Theme)
+
+ exceptions := sync.LoadThemeException(sync.GlobalImporter, theme)
+
+ var st []string
+ for _, se := range multierr.Errors(sync.SyncThemeDeep(sync.GlobalImporter, theme, 0, 250, exceptions)) {
+ st = append(st, se.Error())
+ }
+ sync.EditDeepReport(&sync.SyncReport{Themes: map[string][]string{theme.Name: st}}, false)
+ sync.DeepSyncProgress = 255
+ lastSyncError = ""
+ c.JSON(http.StatusOK, st)
+ })
+
+ // Auto sync: to use with continuous deployment, in a development env
+ apiSyncRoutes.POST("/auto/*p", autoSync)
+
+ // Themes
+ apiSyncRoutes.POST("/fixurlids", fixAllURLIds)
+
+ apiSyncRoutes.POST("/themes", func(c *gin.Context) {
+ _, errs := sync.SyncThemes(sync.GlobalImporter)
+ lastSyncError = ""
+ c.JSON(http.StatusOK, flatifySyncErrors(errs))
+ })
+
+ apiSyncThemesRoutes := apiSyncRoutes.Group("/themes/:thid")
+ apiSyncThemesRoutes.Use(ThemeHandler)
+ apiSyncThemesRoutes.POST("/fixurlid", func(c *gin.Context) {
+ theme := c.MustGet("theme").(*fic.Theme)
+ if theme.FixURLId() {
+ v, err := theme.Update()
+ if err != nil {
+ log.Println("Unable to UpdateTheme after fixurlid:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when saving the theme."})
+ return
+ }
+
+ c.JSON(http.StatusOK, v)
+ } else {
+ c.AbortWithStatusJSON(http.StatusOK, 0)
+ }
+ })
+
+ // Exercices
+ declareSyncExercicesRoutes(apiSyncRoutes)
+ declareSyncExercicesRoutes(apiSyncThemesRoutes)
+
+ // Videos sync imports resolution.mp4 from path stored in database.
+ apiSyncRoutes.POST("/videos", func(c *gin.Context) {
+ exercices, err := fic.GetExercices()
+ if err != nil {
+ log.Println("Unable to GetExercices:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve exercices list."})
+ return
+ }
+
+ for _, e := range exercices {
+ if len(e.VideoURI) == 0 || !strings.HasPrefix(e.VideoURI, "$RFILES$/") {
+ continue
+ }
+
+ vpath, err := url.PathUnescape(strings.TrimPrefix(e.VideoURI, "$RFILES$/"))
+ if err != nil {
+ c.JSON(http.StatusExpectationFailed, gin.H{"errmsg": fmt.Sprintf("Unable to perform URL unescape: %s", err.Error())})
+ return
+ }
+
+ _, err = sync.ImportFile(sync.GlobalImporter, vpath, func(filePath, URI string) (interface{}, error) {
+ e.VideoURI = path.Join("$FILES$", strings.TrimPrefix(filePath, fic.FilesDir))
+ return e.Update()
+ })
+ if err != nil {
+ c.JSON(http.StatusExpectationFailed, gin.H{"errmsg": err.Error()})
+ return
+ }
+ }
+
+ c.JSON(http.StatusOK, true)
+ })
+
+ // Remove soluces from the database.
+ apiSyncRoutes.POST("/drop_soluces", func(c *gin.Context) {
+ exercices, err := fic.GetExercices()
+ if err != nil {
+ log.Println("Unable to GetExercices:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve exercices list."})
+ return
+ }
+
+ var errs error
+ for _, e := range exercices {
+ // Remove any published video
+ if len(e.VideoURI) > 0 && strings.HasPrefix(e.VideoURI, "$FILES$") {
+ vpath := path.Join(fic.FilesDir, strings.TrimPrefix(e.VideoURI, "$FILES$/"))
+ err = os.Remove(vpath)
+ if err != nil {
+ errs = multierr.Append(errs, fmt.Errorf("unable to delete published video (%q): %w", e.VideoURI, err))
+ }
+ }
+
+ // Clean the database
+ if len(e.VideoURI) > 0 || len(e.Resolution) > 0 {
+ e.VideoURI = ""
+ e.Resolution = ""
+
+ _, err = e.Update()
+ if err != nil {
+ errs = multierr.Append(errs, fmt.Errorf("unable to update exercice (%d: %s): %w", e.Id, e.Title, err))
+ }
+ }
+ }
+
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"errmsg": flatifySyncErrors(err)})
+ } else {
+ c.JSON(http.StatusOK, true)
+ }
+ })
+}
+
+func declareSyncExercicesRoutes(router *gin.RouterGroup) {
+ router.POST("/exercices", func(c *gin.Context) {
+ theme := c.MustGet("theme").(*fic.Theme)
+ exceptions := sync.LoadThemeException(sync.GlobalImporter, theme)
+
+ _, errs := sync.SyncExercices(sync.GlobalImporter, theme, exceptions)
+ c.JSON(http.StatusOK, flatifySyncErrors(errs))
+ })
+ apiSyncExercicesRoutes := router.Group("/exercices/:eid")
+ apiSyncExercicesRoutes.Use(ExerciceHandler)
+ apiSyncExercicesRoutes.POST("", func(c *gin.Context) {
+ theme := c.MustGet("theme").(*fic.Theme)
+ exercice := c.MustGet("exercice").(*fic.Exercice)
+
+ exceptions := sync.LoadExerciceException(sync.GlobalImporter, theme, exercice, nil)
+
+ _, _, _, errs := sync.SyncExercice(sync.GlobalImporter, theme, exercice.Path, nil, exceptions)
+ c.JSON(http.StatusOK, flatifySyncErrors(errs))
+ })
+ apiSyncExercicesRoutes.POST("/files", func(c *gin.Context) {
+ exercice := c.MustGet("exercice").(*fic.Exercice)
+ theme := c.MustGet("theme").(*fic.Theme)
+
+ exceptions := sync.LoadExerciceException(sync.GlobalImporter, theme, exercice, nil)
+
+ c.JSON(http.StatusOK, flatifySyncErrors(sync.ImportExerciceFiles(sync.GlobalImporter, exercice, exceptions)))
+ })
+ apiSyncExercicesRoutes.POST("/fixurlid", func(c *gin.Context) {
+ exercice := c.MustGet("exercice").(*fic.Exercice)
+ if exercice.FixURLId() {
+ v, err := exercice.Update()
+ if err != nil {
+ log.Println("Unable to UpdateExercice after fixurlid:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when saving the exercice."})
+ return
+ }
+
+ c.JSON(http.StatusOK, v)
+ } else {
+ c.AbortWithStatusJSON(http.StatusOK, 0)
+ }
+ })
+ apiSyncExercicesRoutes.POST("/hints", func(c *gin.Context) {
+ exercice := c.MustGet("exercice").(*fic.Exercice)
+ theme := c.MustGet("theme").(*fic.Theme)
+
+ exceptions := sync.LoadExerciceException(sync.GlobalImporter, theme, exercice, nil)
+
+ _, errs := sync.SyncExerciceHints(sync.GlobalImporter, exercice, sync.ExerciceFlagsMap(sync.GlobalImporter, exercice), exceptions)
+ c.JSON(http.StatusOK, flatifySyncErrors(errs))
+ })
+ apiSyncExercicesRoutes.POST("/flags", func(c *gin.Context) {
+ exercice := c.MustGet("exercice").(*fic.Exercice)
+ theme := c.MustGet("theme").(*fic.Theme)
+
+ exceptions := sync.LoadExerciceException(sync.GlobalImporter, theme, exercice, nil)
+ _, errs := sync.SyncExerciceFlags(sync.GlobalImporter, exercice, exceptions)
+ _, herrs := sync.SyncExerciceHints(sync.GlobalImporter, exercice, sync.ExerciceFlagsMap(sync.GlobalImporter, exercice), exceptions)
+ c.JSON(http.StatusOK, flatifySyncErrors(multierr.Append(errs, herrs)))
+ })
+}
+
+// autoSync tries to performs a smart synchronization, when in development environment.
+// It'll sync most of modified things, and will delete out of sync data.
+// Avoid using it in a production environment.
+func autoSync(c *gin.Context) {
+ p := strings.Split(strings.TrimPrefix(c.Params.ByName("p"), "/"), "/")
+
+ if !IsProductionEnv {
+ if err := sync.GlobalImporter.Sync(); err != nil {
+ lastSyncError = err.Error()
+ log.Println("Unable to sync.GI.Sync:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to perform the pull."})
+ return
+ }
+ lastSyncError = ""
+ }
+
+ themes, err := fic.GetThemes()
+ if err != nil {
+ log.Println("Unable to GetThemes:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve theme list."})
+ return
+ }
+
+ // No argument, do a deep sync
+ if len(p) == 0 {
+ if !IsProductionEnv {
+ for _, theme := range themes {
+ theme.DeleteDeep()
+ }
+ }
+
+ st := sync.SyncDeep(sync.GlobalImporter)
+ c.JSON(http.StatusOK, st)
+ return
+ }
+
+ var theTheme *fic.Theme
+
+ // Find the given theme
+ for _, theme := range themes {
+ if theme.Path == p[0] {
+ theTheme = theme
+ break
+ }
+ }
+
+ if theTheme == nil {
+ // The theme doesn't exists locally, perhaps it has not been imported already?
+ rThemes, err := sync.GetThemes(sync.GlobalImporter)
+ if err == nil {
+ for _, theme := range rThemes {
+ if theme == p[0] {
+ sync.SyncThemes(sync.GlobalImporter)
+
+ themes, err := fic.GetThemes()
+ if err == nil {
+ for _, theme := range themes {
+ if theme.Path == p[0] {
+ theTheme = theme
+ break
+ }
+ }
+ }
+
+ break
+ }
+ }
+ }
+
+ if theTheme == nil {
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": fmt.Sprintf("Theme not found %q", p[0])})
+ return
+ }
+ }
+
+ if !IsProductionEnv {
+ exercices, err := theTheme.GetExercices()
+ if err == nil {
+ for _, exercice := range exercices {
+ if len(p) <= 1 || exercice.Path == path.Join(p[0], p[1]) {
+ exercice.DeleteDeep()
+ }
+ }
+ }
+ }
+
+ exceptions := sync.LoadThemeException(sync.GlobalImporter, theTheme)
+
+ var st []string
+ for _, se := range multierr.Errors(sync.SyncThemeDeep(sync.GlobalImporter, theTheme, 0, 250, exceptions)) {
+ st = append(st, se.Error())
+ }
+ sync.EditDeepReport(&sync.SyncReport{Themes: map[string][]string{theTheme.Name: st}}, false)
+ sync.DeepSyncProgress = 255
+
+ resp, err := generation.FullGeneration()
+ if err == nil {
+ defer resp.Body.Close()
+ }
+
+ c.JSON(http.StatusOK, st)
+}
+
+func diffDBWithRemote() (map[string][]syncDiff, error) {
+ diffs := map[string][]syncDiff{}
+
+ themes, err := fic.GetThemesExtended()
+ if err != nil {
+ return nil, err
+ }
+
+ // Compare inner themes
+ for _, theme := range themes {
+ diffs[theme.Name], err = diffThemeWithRemote(theme)
+ if err != nil {
+ return nil, fmt.Errorf("Unable to diffThemeWithRemote: %w", err)
+ }
+ }
+
+ return diffs, err
+}
+
+func APIDiffDBWithRemote(c *gin.Context) {
+ diffs, err := diffDBWithRemote()
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, diffs)
+}
diff --git a/admin/api/team.go b/admin/api/team.go
index 4671f6ec..d5d21992 100644
--- a/admin/api/team.go
+++ b/admin/api/team.go
@@ -3,154 +3,639 @@ package api
import (
"encoding/json"
"fmt"
+ "log"
+ "net/http"
+ "strconv"
"strings"
+ "time"
+ "srs.epita.fr/fic-server/admin/pki"
"srs.epita.fr/fic-server/libfic"
+
+ "github.com/gin-gonic/gin"
)
-func init() {
- rts := router.PathPrefix("/teams").Subrouter()
- router.Path("/teams.json").Methods("GET").HandlerFunc(apiHandler(
- func(map[string]string,[]byte) (interface{}, error) {
- return fic.ExportTeams() }))
- rts.Path("/").Methods("GET").HandlerFunc(apiHandler(
- func(map[string]string,[]byte) (interface{}, error) {
- return fic.GetTeams() }))
- rts.Path("/binding").Methods("GET").HandlerFunc(apiHandler(
- func(map[string]string,[]byte) (interface{}, error) {
- return bindingTeams() }))
- rts.Path("/nginx").Methods("GET").HandlerFunc(apiHandler(
- func(map[string]string,[]byte) (interface{}, error) {
- return nginxGenTeam() }))
- rts.Path("/nginx-members").Methods("GET").HandlerFunc(apiHandler(
- func(map[string]string,[]byte) (interface{}, error) {
- return nginxGenMember() }))
- rts.Path("/binding").Methods("GET").HandlerFunc(apiHandler(
- func(map[string]string,[]byte) (interface{}, error) {
- return fic.GetTries(nil, nil) }))
+func declareTeamsRoutes(router *gin.RouterGroup) {
+ router.GET("/teams.json", func(c *gin.Context) {
+ teams, err := fic.ExportTeams(false)
+ if err != nil {
+ log.Println("Unable to ExportTeams:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during teams export."})
+ return
+ }
- rts.Path("/0/my.json").Methods("GET").HandlerFunc(apiHandler(
- func(map[string]string,[]byte) (interface{}, error) {
- return fic.MyJSONTeam(nil, true) }))
- rts.Path("/0/wait.json").Methods("GET").HandlerFunc(apiHandler(
- func(map[string]string,[]byte) (interface{}, error) {
- return fic.MyJSONTeam(nil, false) }))
- rts.Path("/0/stats.json").Methods("GET").HandlerFunc(apiHandler(
- func(map[string]string,[]byte) (interface{}, error) {
- return fic.GetTeamsStats(nil) }))
- rts.Path("/0/tries").Methods("GET").HandlerFunc(apiHandler(
- func(map[string]string,[]byte) (interface{}, error) {
- return fic.GetTries(nil, nil) }))
+ c.JSON(http.StatusOK, teams)
+ })
+ router.GET("/teams-members.json", func(c *gin.Context) {
+ teams, err := fic.ExportTeams(true)
+ if err != nil {
+ log.Println("Unable to ExportTeams:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during teams export."})
+ return
+ }
- rt := rts.PathPrefix("/{tid}").Subrouter()
- rt.Path("/").Methods("GET").HandlerFunc(apiHandler(teamHandler(
- func(team fic.Team, args map[string]string, body []byte) (interface{}, error) {
- return team, nil })))
- rt.Path("/").Methods("DELETE").HandlerFunc(apiHandler(teamHandler(
- func(team fic.Team, args map[string]string, body []byte) (interface{}, error) {
- return team.Delete() })))
- rt.Path("/my.json").Methods("GET").HandlerFunc(apiHandler(teamHandler(
- func(team fic.Team, args map[string]string, body []byte) (interface{}, error) {
- return fic.MyJSONTeam(&team, true) })))
- rt.Path("/wait.json").Methods("GET").HandlerFunc(apiHandler(teamHandler(
- func(team fic.Team, args map[string]string, body []byte) (interface{}, error) {
- return fic.MyJSONTeam(&team, false) })))
- rt.Path("/stats.json").Methods("GET").HandlerFunc(apiHandler(teamHandler(
- func(team fic.Team, args map[string]string, body []byte) (interface{}, error) {
- return team.GetStats() })))
- rt.Path("/tries").Methods("GET").HandlerFunc(apiHandler(teamHandler(
- func(team fic.Team, args map[string]string, body []byte) (interface{}, error) {
- return fic.GetTries(&team, nil) })))
- rt.Path("/members").Methods("GET").HandlerFunc(apiHandler(teamHandler(
- func(team fic.Team, args map[string]string, body []byte) (interface{}, error) {
- return team.GetMembers() })))
- rt.Path("/name").Methods("GET").HandlerFunc(apiHandler(teamHandler(
- func(team fic.Team, args map[string]string, body []byte) (interface{}, error) {
- return team.Name, nil })))
+ c.JSON(http.StatusOK, teams)
+ })
+ router.GET("/teams-associations.json", allAssociations)
+ router.GET("/teams-binding", bindingTeams)
+ router.GET("/teams-nginx", nginxGenTeams)
+ router.POST("/refine_colors", refineTeamsColors)
+ router.POST("/disableinactiveteams", disableInactiveTeams)
+ router.POST("/enableallteams", enableAllTeams)
+ router.GET("/teams-members-nginx", nginxGenMember)
+ router.GET("/teams-tries.json", func(c *gin.Context) {
+ tries, err := fic.GetTries(nil, nil)
+ if err != nil {
+ log.Println("Unable to GetTries:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieves tries."})
+ return
+ }
+
+ c.JSON(http.StatusOK, tries)
+ })
+
+ router.GET("/teams", func(c *gin.Context) {
+ teams, err := fic.GetTeams()
+ if err != nil {
+ log.Println("Unable to GetTeams:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during teams listing."})
+ return
+ }
+
+ c.JSON(http.StatusOK, teams)
+ })
+ router.POST("/teams", createTeam)
+
+ apiTeamsRoutes := router.Group("/teams/:tid")
+ apiTeamsRoutes.Use(TeamHandler)
+ apiTeamsRoutes.GET("/", func(c *gin.Context) {
+ c.JSON(http.StatusOK, c.MustGet("team").(*fic.Team))
+ })
+ apiTeamsRoutes.PUT("/", updateTeam)
+ apiTeamsRoutes.POST("/", addTeamMember)
+ apiTeamsRoutes.DELETE("/", deleteTeam)
+ apiTeamsRoutes.GET("/score-grid.json", func(c *gin.Context) {
+ team := c.MustGet("team").(*fic.Team)
+
+ sg, err := team.ScoreGrid()
+ if err != nil {
+ log.Printf("Unable to get ScoreGrid(tid=%d): %s", team.Id, err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during score grid calculation."})
+ return
+ }
+
+ c.JSON(http.StatusOK, sg)
+ })
+
+ apiTeamsPublicRoutes := router.Group("/teams/:tid")
+ apiTeamsPublicRoutes.Use(TeamPublicHandler)
+ apiTeamsPublicRoutes.GET("/my.json", func(c *gin.Context) {
+ var team *fic.Team
+ if t, ok := c.Get("team"); ok && t != nil {
+ team = t.(*fic.Team)
+ }
+ tfile, err := fic.MyJSONTeam(team, true)
+ if err != nil {
+ log.Println("Unable to get MyJSONTeam:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during team JSON generation."})
+ return
+ }
+
+ c.JSON(http.StatusOK, tfile)
+ })
+ apiTeamsPublicRoutes.GET("/wait.json", func(c *gin.Context) {
+ var team *fic.Team
+ if t, ok := c.Get("team"); ok && t != nil {
+ team = t.(*fic.Team)
+ }
+ tfile, err := fic.MyJSONTeam(team, false)
+ if err != nil {
+ log.Println("Unable to get MyJSONTeam:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during team JSON generation."})
+ return
+ }
+
+ c.JSON(http.StatusOK, tfile)
+ })
+ apiTeamsPublicRoutes.GET("/stats.json", func(c *gin.Context) {
+ var team *fic.Team
+ if t, ok := c.Get("team"); ok && t != nil {
+ team = t.(*fic.Team)
+ }
+ if team != nil {
+ stats, err := team.GetStats()
+ if err != nil {
+ log.Println("Unable to get GetStats:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during stats calculation."})
+ return
+ }
+
+ c.JSON(http.StatusOK, stats)
+ } else {
+ stats, err := fic.GetTeamsStats(nil)
+ if err != nil {
+ log.Println("Unable to get GetTeamsStats:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during global stats calculation."})
+ return
+ }
+
+ c.JSON(http.StatusOK, stats)
+ }
+ })
+ apiTeamsRoutes.GET("/history.json", func(c *gin.Context) {
+ team := c.MustGet("team").(*fic.Team)
+
+ history, err := team.GetHistory()
+ if err != nil {
+ log.Println("Unable to get GetHistory:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during history calculation."})
+ return
+ }
+
+ c.JSON(http.StatusOK, history)
+ })
+ apiTeamsRoutes.PATCH("/history.json", updateHistory)
+ apiTeamsRoutes.DELETE("/history.json", delHistory)
+ apiTeamsPublicRoutes.GET("/tries", func(c *gin.Context) {
+ team := c.MustGet("team").(*fic.Team)
+
+ tries, err := fic.GetTries(team, nil)
+ if err != nil {
+ log.Println("Unable to GetTries:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during tries calculation."})
+ return
+ }
+
+ c.JSON(http.StatusOK, tries)
+ })
+ apiTeamsRoutes.GET("/members", func(c *gin.Context) {
+ team := c.MustGet("team").(*fic.Team)
+
+ members, err := team.GetMembers()
+ if err != nil {
+ log.Println("Unable to GetMembers:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during members retrieval."})
+ return
+ }
+
+ c.JSON(http.StatusOK, members)
+ })
+ apiTeamsRoutes.POST("/members", addTeamMember)
+ apiTeamsRoutes.PUT("/members", setTeamMember)
+
+ declareTeamsPasswordRoutes(apiTeamsRoutes)
+ declareTeamClaimsRoutes(apiTeamsRoutes)
+ declareTeamCertificateRoutes(apiTeamsRoutes)
+
+ // Import teams from cyberrange
+ router.POST("/cyberrange-teams.json", importTeamsFromCyberrange)
}
-func nginxGenMember() (string, error) {
- if teams, err := fic.GetTeams(); err != nil {
- return "", err
+func TeamHandler(c *gin.Context) {
+ tid, err := strconv.ParseInt(string(c.Params.ByName("tid")), 10, 64)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid team identifier"})
+ return
+ }
+
+ team, err := fic.GetTeam(tid)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Team not found"})
+ return
+ }
+
+ c.Set("team", team)
+
+ c.Next()
+}
+
+func TeamPublicHandler(c *gin.Context) {
+ tid, err := strconv.ParseInt(string(c.Params.ByName("tid")), 10, 64)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid team identifier"})
+ return
+ }
+
+ if tid != 0 {
+ team, err := fic.GetTeam(tid)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Team not found"})
+ return
+ }
+
+ c.Set("team", team)
} else {
- ret := ""
+ c.Set("team", nil)
+ }
+
+ c.Next()
+}
+
+func nginxGenTeams(c *gin.Context) {
+ teams, err := fic.GetTeams()
+ if err != nil {
+ log.Println("Unable to GetTeams:", err.Error())
+ c.AbortWithError(http.StatusInternalServerError, err)
+ return
+ }
+
+ ret := ""
+ for _, team := range teams {
+ ret += fmt.Sprintf(" if ($remote_user = \"%s\") { set $team \"%d\"; }\n", strings.ToLower(team.Name), team.Id)
+ }
+
+ c.String(http.StatusOK, ret)
+}
+
+func nginxGenMember(c *gin.Context) {
+ teams, err := fic.GetTeams()
+ if err != nil {
+ log.Println("Unable to GetTeams:", err.Error())
+ c.AbortWithError(http.StatusInternalServerError, err)
+ return
+ }
+
+ ret := ""
+ for _, team := range teams {
+ if members, err := team.GetMembers(); err == nil {
+ for _, member := range members {
+ ret += fmt.Sprintf(" if ($remote_user = \"%s\") { set $team \"%d\"; }\n", member.Nickname, team.Id)
+ }
+ } else {
+ c.AbortWithError(http.StatusInternalServerError, err)
+ return
+ }
+ }
+
+ c.String(http.StatusOK, ret)
+}
+
+func bindingTeams(c *gin.Context) {
+ teams, err := fic.GetTeams()
+ if err != nil {
+ log.Println("Unable to GetTeams:", err.Error())
+ c.AbortWithError(http.StatusInternalServerError, err)
+ return
+ }
+
+ ret := ""
+ for _, team := range teams {
+ if members, err := team.GetMembers(); err != nil {
+ c.AbortWithError(http.StatusInternalServerError, err)
+ return
+ } else {
+ var mbs []string
+ for _, member := range members {
+ mbs = append(mbs, fmt.Sprintf("%s %s", member.Firstname, member.Lastname))
+ }
+ ret += fmt.Sprintf("%d;%s;%s\n", team.Id, team.Name, strings.Join(mbs, ";"))
+ }
+ }
+
+ c.String(http.StatusOK, ret)
+}
+
+type teamAssociation struct {
+ Association string `json:"association"`
+ TeamId int64 `json:"team_id"`
+}
+
+func allAssociations(c *gin.Context) {
+ teams, err := fic.GetTeams()
+ if err != nil {
+ log.Println("Unable to GetTeams:", err.Error())
+ c.AbortWithError(http.StatusInternalServerError, err)
+ return
+ }
+
+ var ret []teamAssociation
+
+ for _, team := range teams {
+ assocs, err := pki.GetTeamAssociations(TeamsDir, team.Id)
+ if err != nil {
+ c.AbortWithError(http.StatusInternalServerError, err)
+ return
+ }
+
+ for _, a := range assocs {
+ ret = append(ret, teamAssociation{a, team.Id})
+ }
+ }
+
+ c.JSON(http.StatusOK, ret)
+}
+
+func importTeamsFromCyberrange(c *gin.Context) {
+ file, err := c.FormFile("file")
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"errmsg": "Failed to get file: " + err.Error()})
+ return
+ }
+
+ src, err := file.Open()
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"errmsg": "Failed to open file: " + err.Error()})
+ return
+ }
+ defer src.Close()
+
+ var ut []fic.CyberrangeTeamBase
+ err = json.NewDecoder(src).Decode(&fic.CyberrangeAPIResponse{Data: &ut})
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ teams, err := fic.GetTeams()
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Impossible de récupérer la liste des équipes actuelles: %s", err.Error())})
+ return
+ }
+
+ for _, crteam := range ut {
+ var exist_team *fic.Team
for _, team := range teams {
- if members, err := team.GetMembers(); err == nil {
- for _, member := range members {
- ret += fmt.Sprintf(" if ($remote_user = \"%s\") { set $team \"%s\"; }\n", member.Nickname, team.InitialName)
- }
- } else {
- return "", err
+ if team.Name == crteam.Name || team.ExternalId == crteam.UUID {
+ exist_team = team
+ break
}
}
- return ret, nil
- }
-}
-
-func nginxGenTeam() (string, error) {
- if teams, err := fic.GetTeams(); err != nil {
- return "", err
- } else {
- ret := ""
- for _, team := range teams {
- ret += fmt.Sprintf(" if ($ssl_client_s_dn ~ \"/C=FR/ST=France/O=Epita/OU=SRS/CN=%s\") { set $team \"%s\"; }\n", team.InitialName, team.InitialName)
+ if exist_team != nil {
+ exist_team.Name = crteam.Name
+ exist_team.ExternalId = crteam.UUID
+ _, err = exist_team.Update()
+ } else {
+ exist_team, err = fic.CreateTeam(crteam.Name, fic.RandomColor().ToRGB(), crteam.UUID)
}
- return ret, nil
- }
-}
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Impossible d'ajouter/de modifier l'équipe %v: %s", crteam, err.Error())})
+ return
+ }
-func bindingTeams() (string, error) {
- if teams, err := fic.GetTeams(); err != nil {
- return "", err
- } else {
- ret := ""
- for _, team := range teams {
- if members, err := team.GetMembers(); err != nil {
- return "", err
- } else {
- var mbs []string
- for _, member := range members {
- mbs = append(mbs, fmt.Sprintf("%s %s", member.Firstname, member.Lastname))
+ // Import members
+ if c.DefaultQuery("nomembers", "0") != "" && len(crteam.Members) > 0 {
+ exist_team.ClearMembers()
+
+ for _, member := range crteam.Members {
+ _, err = exist_team.AddMember(member.Name, "", member.Nickname, exist_team.Name)
+ if err != nil {
+ log.Printf("Unable to add member %q to team %s (tid=%d): %s", member.UUID, exist_team.Name, exist_team.Id, err.Error())
}
- ret += fmt.Sprintf("%d;%s;%s\n", team.Id, team.Name, strings.Join(mbs, ";"))
}
}
- return ret, nil
- }
-}
-
-type uploadedTeam struct {
- Name string
- Color uint32
-}
-
-type uploadedMember struct {
- Firstname string
- Lastname string
- Nickname string
- Company string
-}
-
-func createTeam(args map[string]string, body []byte) (interface{}, error) {
- var ut uploadedTeam
- if err := json.Unmarshal(body, &ut); err != nil {
- return nil, err
}
- return fic.CreateTeam(ut.Name, ut.Color)
+ teams, err = fic.GetTeams()
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Impossible de récupérer la liste des équipes après import: %s", err.Error())})
+ return
+ }
+
+ c.JSON(http.StatusOK, teams)
}
-func addTeamMember(team fic.Team, args map[string]string, body []byte) (interface{}, error) {
- var members []uploadedMember
- if err := json.Unmarshal(body, &members); err != nil {
- return nil, err
+func createTeam(c *gin.Context) {
+ var ut fic.Team
+ err := c.ShouldBindJSON(&ut)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ if ut.Color == 0 {
+ ut.Color = fic.RandomColor().ToRGB()
+ }
+
+ team, err := fic.CreateTeam(strings.TrimSpace(ut.Name), ut.Color, ut.ExternalId)
+ if err != nil {
+ log.Println("Unable to CreateTeam:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during team creation."})
+ return
+ }
+
+ c.JSON(http.StatusOK, team)
+}
+
+func updateTeam(c *gin.Context) {
+ team := c.MustGet("team").(*fic.Team)
+
+ var ut fic.Team
+ err := c.ShouldBindJSON(&ut)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ ut.Id = team.Id
+
+ if ut.Password != nil && *ut.Password == "" {
+ ut.Password = nil
+ }
+
+ _, err = ut.Update()
+ if err != nil {
+ log.Println("Unable to updateTeam:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during team updating."})
+ return
+ }
+
+ c.JSON(http.StatusOK, ut)
+}
+
+func refineTeamsColors(c *gin.Context) {
+ teams, err := fic.GetTeams()
+ if err != nil {
+ log.Println("Unable to GetTeams:", err.Error())
+ c.AbortWithError(http.StatusInternalServerError, err)
+ return
+ }
+
+ for i, team := range teams {
+ team.Color = fic.HSL{
+ H: float64(i)/float64(len(teams)) - 0.2,
+ S: float64(1) / float64(1+i%2),
+ L: 0.25 + float64(0.5)/float64(1+i%3),
+ }.ToRGB()
+
+ _, err = team.Update()
+ if err != nil {
+ c.AbortWithError(http.StatusInternalServerError, err)
+ return
+ }
+ }
+
+ c.JSON(http.StatusOK, teams)
+}
+
+func disableInactiveTeams(c *gin.Context) {
+ teams, err := fic.GetTeams()
+ if err != nil {
+ log.Println("Unable to GetTeams:", err.Error())
+ c.AbortWithError(http.StatusInternalServerError, err)
+ return
+ }
+
+ for _, team := range teams {
+ var serials []uint64
+ serials, err = pki.GetTeamSerials(TeamsDir, team.Id)
+ if err != nil {
+ c.AbortWithError(http.StatusInternalServerError, err)
+ return
+ }
+
+ var assocs []string
+ assocs, err = pki.GetTeamAssociations(TeamsDir, team.Id)
+ if err != nil {
+ c.AbortWithError(http.StatusInternalServerError, err)
+ return
+ }
+
+ if len(serials) == 0 && len(assocs) == 0 {
+ if team.Active {
+ team.Active = false
+ team.Update()
+ }
+ } else if !team.Active {
+ team.Active = true
+ team.Update()
+ }
+ }
+
+ c.JSON(http.StatusOK, true)
+}
+
+func enableAllTeams(c *gin.Context) {
+ teams, err := fic.GetTeams()
+ if err != nil {
+ log.Println("Unable to GetTeams:", err.Error())
+ c.AbortWithError(http.StatusInternalServerError, err)
+ return
+ }
+
+ for _, team := range teams {
+ if !team.Active {
+ team.Active = true
+ team.Update()
+ }
+ }
+
+ c.JSON(http.StatusOK, true)
+}
+
+func deleteTeam(c *gin.Context) {
+ team := c.MustGet("team").(*fic.Team)
+
+ assocs, err := pki.GetTeamAssociations(TeamsDir, team.Id)
+ if err != nil {
+ log.Printf("Unable to GetTeamAssociations(tid=%d): %s", team.Id, err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to retrieve team association."})
+ return
+ }
+
+ for _, assoc := range assocs {
+ err = pki.DeleteTeamAssociation(TeamsDir, assoc)
+ if err != nil {
+ log.Printf("Unable to DeleteTeamAssociation(assoc=%s): %s", assoc, err.Error())
+ return
+ }
+ }
+
+ _, err = team.Delete()
+ if err != nil {
+ log.Println("Unable to deleteTeam:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during team deletion."})
+ return
+ }
+
+ c.JSON(http.StatusOK, true)
+}
+
+func addTeamMember(c *gin.Context) {
+ team := c.MustGet("team").(*fic.Team)
+
+ var members []fic.Member
+ err := c.ShouldBindJSON(&members)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
}
for _, member := range members {
- team.AddMember(member.Firstname, member.Lastname, member.Nickname, member.Company)
+ _, err := team.AddMember(strings.TrimSpace(member.Firstname), strings.TrimSpace(member.Lastname), strings.TrimSpace(member.Nickname), strings.TrimSpace(member.Company))
+ if err != nil {
+ log.Println("Unable to AddMember:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during member creation."})
+ return
+ }
}
- return team.GetMembers()
+ mmbrs, err := team.GetMembers()
+ if err != nil {
+ log.Println("Unable to retrieve members list:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve members list."})
+ return
+ }
+
+ c.JSON(http.StatusOK, mmbrs)
+}
+
+func setTeamMember(c *gin.Context) {
+ team := c.MustGet("team").(*fic.Team)
+ team.ClearMembers()
+ addTeamMember(c)
+}
+
+type uploadedHistory struct {
+ Kind string
+ Time time.Time
+ Primary *int64
+ Secondary *int64
+ Coefficient float32
+}
+
+func updateHistory(c *gin.Context) {
+ team := c.MustGet("team").(*fic.Team)
+
+ var uh uploadedHistory
+ err := c.ShouldBindJSON(&uh)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ var givenId int64
+ if uh.Secondary != nil {
+ givenId = *uh.Secondary
+ } else if uh.Primary != nil {
+ givenId = *uh.Primary
+ }
+
+ _, err = team.UpdateHistoryCoeff(uh.Kind, uh.Time, givenId, uh.Coefficient)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to update this history line: %s", err.Error())})
+ return
+ }
+
+ c.JSON(http.StatusOK, true)
+}
+
+func delHistory(c *gin.Context) {
+ team := c.MustGet("team").(*fic.Team)
+
+ var uh uploadedHistory
+ err := c.ShouldBindJSON(&uh)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ _, err = team.DelHistoryItem(uh.Kind, uh.Time, uh.Primary, uh.Secondary)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to delete this history line: %s", err.Error())})
+ return
+ }
+
+ c.JSON(http.StatusOK, true)
}
diff --git a/admin/api/theme.go b/admin/api/theme.go
index 7223861f..c1057397 100644
--- a/admin/api/theme.go
+++ b/admin/api/theme.go
@@ -1,161 +1,376 @@
package api
import (
- "encoding/json"
- "errors"
"fmt"
+ "log"
+ "net/http"
+ "path"
+ "reflect"
"strconv"
+ "strings"
+ "srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic"
+ "srs.epita.fr/fic-server/settings"
+
+ "github.com/gin-gonic/gin"
)
-func init() {
- router.Path("/themes/").Methods("GET").HandlerFunc(apiHandler(listThemes))
- router.Path("/themes/").Methods("POST").HandlerFunc(apiHandler(createTheme))
- router.Path("/themes.json").Methods("GET").HandlerFunc(apiHandler(exportThemes))
- router.Path("/themes/files-bindings").Methods("GET").HandlerFunc(apiHandler(bindingFiles))
+func declareThemesRoutes(router *gin.RouterGroup) {
+ router.GET("/themes", listThemes)
+ router.POST("/themes", createTheme)
+ router.GET("/themes.json", exportThemes)
+ router.GET("/session-forensic.yaml", func(c *gin.Context) {
+ if s, err := settings.ReadSettings(path.Join(settings.SettingsDir, settings.SettingsFile)); err != nil {
+ log.Printf("Unable to ReadSettings: %s", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during settings reading."})
+ return
- rt := router.PathPrefix("/themes/{tid:[0-9]+}").Subrouter()
- rt.Path("/").Methods("GET").HandlerFunc(apiHandler(themeHandler(showTheme)))
- rt.Path("/").Methods("PUT").HandlerFunc(apiHandler(themeHandler(updateTheme)))
- rt.Path("/").Methods("DELETE").HandlerFunc(apiHandler(themeHandler(deleteTheme)))
-
- rtes := rt.PathPrefix("/exercices").Subrouter()
- rtes.Path("/").Methods("GET").HandlerFunc(apiHandler(themeHandler(listThemedExercices)))
- rtes.Path("/").Methods("POST").HandlerFunc(apiHandler(themeHandler(createExercice)))
-
- rte := rtes.PathPrefix("/{eid:[0-9]+}").Subrouter()
- rte.Path("/").Methods("GET").HandlerFunc(apiHandler(themeHandler(themedExerciceHandler(showThemedExercice))))
- rte.Path("/").Methods("PUT").HandlerFunc(apiHandler(themeHandler(themedExerciceHandler(updateThemedExercice))))
- rte.Path("/").Methods("DELETE").HandlerFunc(apiHandler(themeHandler(themedExerciceHandler(deleteThemedExercice))))
-
- rtef := rte.Path("/files").Subrouter()
- rtef.Methods("GET").HandlerFunc(apiHandler(themeHandler(themedExerciceHandler(listThemedExerciceFiles))))
- rtef.Methods("POST").HandlerFunc(apiHandler(themeHandler(themedExerciceHandler(createExerciceFile))))
-
- rteh := rte.Path("/hints").Subrouter()
- rteh.Methods("GET").HandlerFunc(apiHandler(themeHandler(themedExerciceHandler(listThemedExerciceHints))))
- rteh.Methods("POST").HandlerFunc(apiHandler(themeHandler(themedExerciceHandler(createExerciceHint))))
-
- rtek := rte.Path("/keys").Subrouter()
- rtek.Methods("GET").HandlerFunc(apiHandler(themeHandler(themedExerciceHandler(listThemedExerciceKeys))))
- rtek.Methods("POST").HandlerFunc(apiHandler(themeHandler(themedExerciceHandler(createExerciceKey))))
-}
-
-func bindingFiles(args map[string]string, body []byte) (interface{}, error) {
- if files, err := fic.GetFiles(); err != nil {
- return "", err
- } else {
- ret := ""
- for _, file := range files {
- ret += fmt.Sprintf("%s;%s\n", file.GetOrigin(), file.Path)
+ } else if challengeinfo, err := sync.GetFileContent(sync.GlobalImporter, "challenge.json"); err != nil {
+ log.Println("Unable to retrieve challenge.json:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to retrive challenge.json: %s", err.Error())})
+ return
+ } else if ch, err := settings.ReadChallengeInfo(challengeinfo); err != nil {
+ log.Printf("Unable to ReadChallengeInfo: %s", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during challenge info reading."})
+ return
+ } else if sf, err := fic.GenZQDSSessionFile(ch, s); err != nil {
+ log.Printf("Unable to GenZQDSSessionFile: %s", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during session file generation."})
+ return
+ } else {
+ c.JSON(http.StatusOK, sf)
}
- return ret, nil
- }
+ })
+ router.GET("/files-bindings", bindingFiles)
+
+ apiThemesRoutes := router.Group("/themes/:thid")
+ apiThemesRoutes.Use(ThemeHandler)
+ apiThemesRoutes.GET("", showTheme)
+ apiThemesRoutes.PUT("", updateTheme)
+ apiThemesRoutes.DELETE("", deleteTheme)
+
+ apiThemesRoutes.POST("/diff-sync", APIDiffThemeWithRemote)
+
+ apiThemesRoutes.GET("/exercices_stats.json", getThemedExercicesStats)
+
+ declareExercicesRoutes(apiThemesRoutes)
+
+ // Remote
+ router.GET("/remote/themes", sync.ApiListRemoteThemes)
+ router.GET("/remote/themes/:thid", sync.ApiGetRemoteTheme)
+ router.GET("/remote/themes/:thid/exercices", sync.ApiListRemoteExercices)
}
-func getExercice(args []string) (fic.Exercice, error) {
- if tid, err := strconv.Atoi(string(args[0])); err != nil {
- return fic.Exercice{}, err
- } else if theme, err := fic.GetTheme(tid); err != nil {
- return fic.Exercice{}, err
- } else if eid, err := strconv.Atoi(string(args[1])); err != nil {
- return fic.Exercice{}, err
+type Theme struct {
+ *fic.Theme
+ ForgeLink string `json:"forge_link,omitempty"`
+}
+
+func ThemeHandler(c *gin.Context) {
+ thid, err := strconv.ParseInt(string(c.Params.ByName("thid")), 10, 64)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid theme identifier"})
+ return
+ }
+
+ if thid == 0 {
+ c.Set("theme", &fic.StandaloneExercicesTheme)
} else {
- return theme.GetExercice(eid)
+ theme, err := fic.GetTheme(thid)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Theme not found"})
+ return
+ }
+
+ c.Set("theme", theme)
}
+
+ c.Next()
}
-func listThemes(args map[string]string, body []byte) (interface{}, error) {
- return fic.GetThemes()
+func fixAllURLIds(c *gin.Context) {
+ nbFix := 0
+ if themes, err := fic.GetThemes(); err == nil {
+ for _, theme := range themes {
+ if theme.FixURLId() {
+ theme.Update()
+ nbFix += 1
+ }
+
+ if exercices, err := theme.GetExercices(); err == nil {
+ for _, exercice := range exercices {
+ if exercice.FixURLId() {
+ exercice.Update()
+ nbFix += 1
+ }
+ }
+ }
+ }
+ }
+
+ c.JSON(http.StatusOK, nbFix)
}
-func exportThemes(args map[string]string, body []byte) (interface{}, error) {
- return fic.ExportThemes()
+func bindingFiles(c *gin.Context) {
+ files, err := fic.GetFiles()
+ if err != nil {
+ c.AbortWithError(http.StatusInternalServerError, err)
+ return
+ }
+
+ ret := ""
+ for _, file := range files {
+ ret += fmt.Sprintf("%s;%s\n", file.GetOrigin(), file.Path)
+ }
+
+ c.String(http.StatusOK, ret)
}
-func showTheme(theme fic.Theme, args map[string]string, body []byte) (interface{}, error) {
- return theme, nil
+func listThemes(c *gin.Context) {
+ themes, err := fic.GetThemes()
+ if err != nil {
+ log.Println("Unable to listThemes:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to list themes."})
+ return
+ }
+
+ if has, _ := fic.HasStandaloneExercice(); has {
+ themes = append([]*fic.Theme{&fic.StandaloneExercicesTheme}, themes...)
+ }
+
+ c.JSON(http.StatusOK, themes)
}
-func listThemedExercices(theme fic.Theme, args map[string]string, body []byte) (interface{}, error) {
- return theme.GetExercices()
+func exportThemes(c *gin.Context) {
+ themes, err := fic.ExportThemes()
+ if err != nil {
+ log.Println("Unable to exportthemes:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to export themes."})
+ return
+ }
+
+ c.JSON(http.StatusOK, themes)
}
-func showThemedExercice(theme fic.Theme, exercice fic.Exercice, args map[string]string, body []byte) (interface{}, error) {
- return exercice, nil
+func showTheme(c *gin.Context) {
+ theme := c.MustGet("theme").(*fic.Theme)
+
+ var forgelink string
+ if fli, ok := sync.GlobalImporter.(sync.ForgeLinkedImporter); ok {
+ if u, _ := fli.GetThemeLink(theme); u != nil {
+ forgelink = u.String()
+ }
+ }
+
+ c.JSON(http.StatusOK, Theme{theme, forgelink})
}
-func listThemedExerciceFiles(theme fic.Theme, exercice fic.Exercice, args map[string]string, body []byte) (interface{}, error) {
- return exercice.GetFiles()
-}
-
-func listThemedExerciceHints(theme fic.Theme, exercice fic.Exercice, args map[string]string, body []byte) (interface{}, error) {
- return exercice.GetHints()
-}
-
-func listThemedExerciceKeys(theme fic.Theme, exercice fic.Exercice, args map[string]string, body []byte) (interface{}, error) {
- return exercice.GetKeys()
-}
-
-
-type uploadedTheme struct {
- Name string
- Authors string
-}
-
-func createTheme(args map[string]string, body []byte) (interface{}, error) {
- var ut uploadedTheme
- if err := json.Unmarshal(body, &ut); err != nil {
- return nil, err
+func createTheme(c *gin.Context) {
+ var ut fic.Theme
+ err := c.ShouldBindJSON(&ut)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
}
if len(ut.Name) == 0 {
- return nil, errors.New("Theme's name not filled")
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Theme's name not filled"})
+ return
}
- return fic.CreateTheme(ut.Name, ut.Authors)
+ th, err := fic.CreateTheme(&ut)
+ if err != nil {
+ log.Println("Unable to createTheme:", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during theme creation."})
+ return
+ }
+
+ c.JSON(http.StatusOK, th)
}
-func updateTheme(theme fic.Theme, args map[string]string, body []byte) (interface{}, error) {
+func updateTheme(c *gin.Context) {
+ theme := c.MustGet("theme").(*fic.Theme)
+
var ut fic.Theme
- if err := json.Unmarshal(body, &ut); err != nil {
- return nil, err
+ err := c.ShouldBindJSON(&ut)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
+ return
}
ut.Id = theme.Id
if len(ut.Name) == 0 {
- return nil, errors.New("Theme's name not filled")
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Theme's name not filled"})
+ return
}
- return ut.Update()
+ if _, err := ut.Update(); err != nil {
+ log.Println("Unable to updateTheme:", err.Error())
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "An error occurs during theme update."})
+ return
+ }
+
+ if theme.Locked != ut.Locked {
+ exercices, err := theme.GetExercices()
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+ for _, exercice := range exercices {
+ if exercice.Disabled != ut.Locked {
+ exercice.Disabled = ut.Locked
+ _, err = exercice.Update()
+ if err != nil {
+ log.Println("Unable to enable/disable exercice: ", exercice.Id, err.Error())
+ }
+ }
+ }
+ }
+
+ c.JSON(http.StatusOK, ut)
}
-func updateThemedExercice(theme fic.Theme, exercice fic.Exercice, args map[string]string, body []byte) (interface{}, error) {
- // Update an exercice
- var ue fic.Exercice
- if err := json.Unmarshal(body, &ue); err != nil {
+func deleteTheme(c *gin.Context) {
+ theme := c.MustGet("theme").(*fic.Theme)
+
+ _, err := theme.Delete()
+ if err != nil {
+ log.Println("Unable to deleteTheme:", err.Error())
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "An error occurs during theme deletion."})
+ return
+ }
+
+ c.JSON(http.StatusOK, true)
+}
+
+func getThemedExercicesStats(c *gin.Context) {
+ theme := c.MustGet("theme").(*fic.Theme)
+
+ exercices, err := theme.GetExercices()
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to fetch exercices: %s", err.Error())})
+ return
+ }
+
+ ret := []exerciceStats{}
+ for _, e := range exercices {
+ ret = append(ret, exerciceStats{
+ IdExercice: e.Id,
+ TeamTries: e.TriedTeamCount(),
+ TotalTries: e.TriedCount(),
+ SolvedCount: e.SolvedCount(),
+ FlagSolved: e.FlagSolved(),
+ MCQSolved: e.MCQSolved(),
+ })
+ }
+ c.JSON(http.StatusOK, ret)
+}
+
+func diffThemeWithRemote(theme *fic.Theme) ([]syncDiff, error) {
+ var diffs []syncDiff
+
+ // Compare theme attributes
+ theme_remote, err := sync.GetRemoteTheme(theme.Path)
+ if err != nil {
return nil, err
}
- ue.Id = exercice.Id
+ for _, field := range reflect.VisibleFields(reflect.TypeOf(*theme)) {
+ if ((field.Name == "Image") && path.Base(reflect.ValueOf(*theme_remote).FieldByName(field.Name).String()) != path.Base(reflect.ValueOf(*theme).FieldByName(field.Name).String())) || (field.Name != "Image" && !reflect.ValueOf(*theme_remote).FieldByName(field.Name).Equal(reflect.ValueOf(*theme).FieldByName(field.Name))) {
+ if !field.IsExported() || field.Name == "Id" || field.Name == "IdTheme" || field.Name == "IssueKind" || field.Name == "BackgroundColor" {
+ continue
+ }
- if len(ue.Title) == 0 {
- return nil, errors.New("Exercice's title not filled")
+ diffs = append(diffs, syncDiff{
+ Field: field.Name,
+ Link: fmt.Sprintf("themes/%d", theme.Id),
+ Before: reflect.ValueOf(*theme).FieldByName(field.Name).Interface(),
+ After: reflect.ValueOf(*theme_remote).FieldByName(field.Name).Interface(),
+ })
+ }
}
- if _, err := ue.Update(); err != nil {
- return nil, err
+ // Compare exercices list
+ exercices, err := theme.GetExercices()
+ if err != nil {
+ return nil, fmt.Errorf("Unable to GetExercices: %w", err)
}
- return ue, nil
+ exercices_remote, err := sync.ListRemoteExercices(theme.Path)
+ if err != nil {
+ return nil, fmt.Errorf("Unable to ListRemoteExercices: %w", err)
+ }
+
+ var not_found []string
+ var extra_found []string
+
+ for _, exercice_remote := range exercices_remote {
+ found := false
+ for _, exercice := range exercices {
+ if exercice.Path[strings.Index(exercice.Path, "/")+1:] == exercice_remote {
+ found = true
+ break
+ }
+ }
+
+ if !found {
+ not_found = append(not_found, exercice_remote)
+ }
+ }
+
+ for _, exercice := range exercices {
+ found := false
+ for _, exercice_remote := range exercices_remote {
+ if exercice.Path[strings.Index(exercice.Path, "/")+1:] == exercice_remote {
+ found = true
+ break
+ }
+ }
+
+ if !found {
+ extra_found = append(extra_found, exercice.Path[strings.Index(exercice.Path, "/")+1:])
+ }
+ }
+
+ if len(not_found) > 0 || len(extra_found) > 0 {
+ diffs = append(diffs, syncDiff{
+ Field: "theme.Exercices",
+ Link: fmt.Sprintf("themes/%d", theme.Id),
+ Before: strings.Join(extra_found, ", "),
+ After: strings.Join(not_found, ", "),
+ })
+ }
+
+ // Compare inner exercices
+ for i, exercice := range exercices {
+ exdiffs, err := diffExerciceWithRemote(exercice, theme)
+ if err != nil {
+ return nil, fmt.Errorf("Unable to diffExerciceWithRemote: %w", err)
+ }
+
+ for _, exdiff := range exdiffs {
+ if theme.Id == 0 {
+ exdiff.Field = fmt.Sprintf("exercices[%d].%s", exercice.Id, exdiff.Field)
+ } else {
+ exdiff.Field = fmt.Sprintf("exercices[%d].%s", i, exdiff.Field)
+ }
+ diffs = append(diffs, exdiff)
+ }
+ }
+
+ return diffs, err
}
-func deleteTheme(theme fic.Theme, args map[string]string, body []byte) (interface{}, error) {
- return theme.Delete()
-}
+func APIDiffThemeWithRemote(c *gin.Context) {
+ theme := c.MustGet("theme").(*fic.Theme)
-func deleteThemedExercice(theme fic.Theme, exercice fic.Exercice, args map[string]string, body []byte) (interface{}, error) {
- return exercice.Delete()
+ diffs, err := diffThemeWithRemote(theme)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, diffs)
}
diff --git a/admin/api/version.go b/admin/api/version.go
index eb5a883a..52cb0726 100644
--- a/admin/api/version.go
+++ b/admin/api/version.go
@@ -1,11 +1,15 @@
package api
-import ()
+import (
+ "net/http"
-func init() {
- router.Path("/version").Methods("GET").HandlerFunc(apiHandler(showVersion))
+ "github.com/gin-gonic/gin"
+)
+
+func DeclareVersionRoutes(router *gin.RouterGroup) {
+ router.GET("/version", showVersion)
}
-func showVersion(args map[string]string, body []byte) (interface{}, error) {
- return map[string]interface{}{"version": 0.1}, nil
+func showVersion(c *gin.Context) {
+ c.JSON(http.StatusOK, gin.H{"version": 1.0})
}
diff --git a/admin/app.go b/admin/app.go
new file mode 100644
index 00000000..1ba1910e
--- /dev/null
+++ b/admin/app.go
@@ -0,0 +1,81 @@
+package main
+
+import (
+ "context"
+ "log"
+ "net/http"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/gin-gonic/gin"
+
+ "srs.epita.fr/fic-server/admin/api"
+ "srs.epita.fr/fic-server/settings"
+)
+
+type App struct {
+ router *gin.Engine
+ srv *http.Server
+ cfg *settings.Settings
+ bind string
+}
+
+func NewApp(cfg *settings.Settings, baseURL string, bind string) App {
+ if !cfg.WorkInProgress {
+ gin.SetMode(gin.ReleaseMode)
+ }
+ gin.ForceConsoleColor()
+ router := gin.Default()
+
+ api.DeclareRoutes(router.Group(""))
+
+ var baserouter *gin.RouterGroup
+ if len(baseURL) > 0 {
+ router.GET("/", func(c *gin.Context) {
+ c.Redirect(http.StatusFound, baseURL)
+ })
+ router.GET(filepath.Dir(baseURL)+"/files/*_", func(c *gin.Context) {
+ path := c.Request.URL.Path
+ c.Redirect(http.StatusFound, filepath.Join(baseURL, strings.TrimPrefix(path, filepath.Dir(baseURL))))
+ })
+
+ baserouter = router.Group(baseURL)
+
+ api.DeclareRoutes(baserouter)
+ declareStaticRoutes(baserouter, cfg, baseURL)
+ } else {
+ declareStaticRoutes(router.Group(""), cfg, "")
+ }
+
+ app := App{
+ router: router,
+ bind: bind,
+ }
+
+ return app
+}
+
+func (app *App) Start() {
+ app.srv = &http.Server{
+ Addr: app.bind,
+ Handler: app.router,
+ ReadHeaderTimeout: 15 * time.Second,
+ ReadTimeout: 15 * time.Second,
+ WriteTimeout: 10 * time.Second,
+ IdleTimeout: 30 * time.Second,
+ }
+
+ log.Printf("Ready, listening on %s\n", app.bind)
+ if err := app.srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
+ log.Fatalf("listen: %s\n", err)
+ }
+}
+
+func (app *App) Stop() {
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ if err := app.srv.Shutdown(ctx); err != nil {
+ log.Fatal("Server Shutdown:", err)
+ }
+}
diff --git a/admin/fill_exercices.sh b/admin/fill_exercices.sh
deleted file mode 100755
index 3b65e3fa..00000000
--- a/admin/fill_exercices.sh
+++ /dev/null
@@ -1,207 +0,0 @@
-#!/bin/bash
-
-BASEURL="http://localhost:8081"
-BASEURI="https://owncloud.srs.epita.fr/remote.php/webdav/FIC 2017"
-BASEFILE="/mnt/fic/"
-CLOUDPASS=nemunaire:'p0WJ$&I#OWEtC5UI4@dD'
-
-new_theme() {
- NAME=`echo $1 | sed 's/"/\\\\"/g'`
- AUTHORS=`echo $2 | sed 's/"/\\\\"/g'`
- curl -f -s -d "{\"name\": \"$NAME\", \"authors\": \"$AUTHORS\"}" "${BASEURL}/api/themes/" |
- grep -Eo '"id":[0-9]+,' | grep -Eo "[0-9]+"
-}
-
-new_exercice() {
- THEME="$1"
- TITLE=`echo "$2" | sed 's/"/\\\\"/g'`
- STATEMENT=`echo "$3" | sed 's/"/\\\\"/g' | sed ':a;N;$!ba;s/\n/ /g'`
- DEPEND="$4"
- GAIN="$5"
- VIDEO="$6"
-
- curl -f -s -d "{\"title\": \"$TITLE\", \"statement\": \"$STATEMENT\", \"depend\": $DEPEND, \"gain\": $GAIN, \"videoURI\": \"$VIDEO\"}" "${BASEURL}/api/themes/$THEME/exercices/" |
- grep -Eo '"id":[0-9]+,' | grep -Eo "[0-9]+"
-}
-
-new_file() {
- THEME="$1"
- EXERCICE="$2"
- URI="$3"
- ARGS="$4"
-
- echo "$ARGS" | while read arg
- do
- [ -z "${PARTS}" ] || PARTS="${PARTS},"
- PARTS="${PARTS}\"$arg\""
- done
-
-# curl -f -s -d "{\"URI\": \"${BASEFILE}${URI}\"}" "${BASEURL}/api/themes/$THEME/$EXERCICE/files" |
- curl -f -s -d @- "${BASEURL}/api/themes/$THEME/exercices/$EXERCICE/files" </g'`
- COST="$5"
-
- curl -f -s -d "{\"title\": \"$TITLE\", \"content\": \"$CONTENT\", \"cost\": $COST}" "${BASEURL}/api/themes/$THEME/exercices/$EXERCICE/hints" |
- grep -Eo '"id":[0-9]+,' | grep -Eo "[0-9]+"
-}
-
-new_key() {
- THEME="$1"
- EXERCICE="$2"
- NAME="$3"
- KEY=`echo $4 | sed 's/"/\\\\"/g' | sed 's#\\\\#\\\\\\\\#g'`
-
- curl -f -s -d "{\"name\": \"$NAME\", \"key\": \"$KEY\"}" "${BASEURL}/api/themes/$THEME/exercices/$EXERCICE/keys" |
- grep -Eo '"id":[0-9]+,' | grep -Eo "[0-9]+"
-}
-
-get_dir_from_cloud() {
- curl -f -s -X PROPFIND -u "${CLOUDPASS}" "${BASEURI}$1" | xmllint --format - | grep 'd:href' | sed -E 's/^.*>(.*)<.*$/\1/'
-}
-get_dir() {
- ls "${BASEFILE}$1" 2> /dev/null
-}
-#alias get_dir=get_dir_from_cloud
-
-get_file_from_cloud() {
- curl -f -s -u "${CLOUDPASS}" "${BASEURI}$1" | tr -d '\r'
-}
-get_file() {
- cat "${BASEFILE}$1" 2> /dev/null | tr -d '\r'
-}
-#alias get_file=get_file_from_cloud
-
-unhtmlentities() {
- cat | sed -E 's/%20/ /g' | sed -E "s/%27/'/g" | sed -E 's/%c3%a9/é/g' | sed -E 's/%c3%a8/è/g'
-}
-
-# Theme
-get_dir "" | while read f; do basename "$f"; done | while read THEME_URI
-do
- THM_BASEURI="/${THEME_URI}/"
- THEME_NAME=$(echo "${THEME_URI#*-}" | unhtmlentities)
- THEME_AUTHORS=$(get_file "${THM_BASEURI}/AUTHORS.txt" | sed 's/$/, /' | tr -d '\n' | sed 's/, $//')
- THEME_ID=`new_theme "$THEME_NAME" "$THEME_AUTHORS"`
- if [ -z "$THEME_ID" ]; then
- echo -e "\e[31;01m!!! An error occured during theme add\e[00m"
- continue
- else
- echo -e "\e[33m>>> New theme created:\e[00m $THEME_ID - $THEME_NAME"
- fi
-
- LAST=null
- EXO_NUM=0
- get_dir "${THM_BASEURI}" | while read f; do basename "$f"; done | while read EXO_URI
- do
- case ${EXO_URI} in
- [0-9]-*)
- ;;
- *)
- continue;;
- esac
-
- #EXO_NUM=$((EXO_NUM + 1))
- EXO_NUM=${EXO_URI%-*}
- EXO_NAME=$(echo "${EXO_URI#*-}" | unhtmlentities)
- echo
- echo -e "\e[36m--- Filling exercice ${EXO_NUM} in theme ${THEME_NAME}\e[00m"
-
- EXO_BASEURI="${EXO_URI}/"
-
- EXO_VIDEO=$(get_dir "${THM_BASEURI}${EXO_BASEURI}/resolution/" | grep -E "\.(mov|mkv|mp4|avi|flv|ogv|webm)$" | while read f; do basename $f; done | tail -1)
- [ -n "$EXO_VIDEO" ] && EXO_VIDEO="/resolution${THM_BASEURI}${EXO_BASEURI}resolution/${EXO_VIDEO}"
-
- if [ "${LAST}" = "null" ]; then
- echo ">>> Assuming this exercice has no dependency"
- else
- echo ">>> Assuming this exercice depends on the last entry (id=${LAST})"
- fi
-
- EXO_GAIN=$((3 * (2 ** $EXO_NUM) - 1))
- echo ">>> Using default gain: ${EXO_GAIN} points"
-
- EXO_SCENARIO=$(get_file "${THM_BASEURI}${EXO_BASEURI}/scenario.txt")
-
- EXO_ID=`new_exercice "${THEME_ID}" "${EXO_NAME}" "${EXO_SCENARIO}" "${LAST}" "${EXO_GAIN}" "${EXO_VIDEO}"`
- if [ -z "$EXO_ID" ]; then
- echo -e "\e[31;01m!!! An error occured during exercice add.\e[00m"
- continue
- else
- echo -e "\e[32m>>> New exercice created:\e[00m $EXO_ID - $EXO_NAME"
- fi
-
-
- # Keys
- get_file "${THM_BASEURI}${EXO_BASEURI}/flags.txt" | while read KEYLINE
- do
- KEY_NAME=$(echo "$KEYLINE" | cut -d : -f 1)
- KEY_RAW=$(echo "$KEYLINE" | cut -d : -f 2-)
-
- if [ -z "${KEY_NAME}" ]; then
- KEY_NAME="Flag"
- fi
-
- KEY_ID=`new_key "${THEME_ID}" "${EXO_ID}" "${KEY_NAME}" "${KEY_RAW}"`
- if [ -z "$KEY_ID" ]; then
- echo -e "\e[31;01m!!! An error occured during key import!\e[00m (name=${KEYNAME};raw=${KEY_RAW})"
- else
- echo -e "\e[32m>>> New key added:\e[00m $KEY_ID - $KEY_NAME"
- fi
- done
-
-
- # Hints
- EXO_HINT=$(get_file "${THM_BASEURI}${EXO_BASEURI}/hint.txt")
- if [ -n "$EXO_HINT" ]; then
- HINT_ID=`new_hint "${THEME_ID}" "${EXO_ID}" "Astuce #1" "${EXO_HINT}" "1"`
- if [ -z "$HINT_ID" ]; then
- echo -e "\e[31;01m!!! An error occured during hint import!\e[00m (title=Astuce $1;content=${EXO_HINT};cost=1)"
- else
- echo -e "\e[32m>>> New key added:\e[00m $KEY_ID - $KEY_NAME"
- fi
- fi
-
-
- # Files: splited
- get_dir "${THM_BASEURI}${EXO_BASEURI}files/" | grep -v DIGESTS.txt | grep '[0-9][0-9]$' | sed -E 's/\.?([0-9][0-9])$//' | sort | uniq | while read f; do basename "$f"; done | while read FILE_URI
- do
- PARTS=
- for part in "$(get_dir "${THM_BASEURI}${EXO_BASEURI}files/" | grep "${FILE_URI}" | sort)"
- do
- PARTS="${PARTS}${BASEFILE}${THM_BASEURI}${EXO_BASEURI}files/${part}\n"
- done
- echo "Import splited file ${THM_BASEURI}${EXO_BASEURI}files/${FILE_URI} from `echo ${PART} | tr '\n' ' '`"
-
- FILE_ID=`new_file "${THEME_ID}" "${EXO_ID}" "${THM_BASEURI}${EXO_BASEURI}files/${FILE_URI}" "${PARTS}"`
- if [ -z "$FILE_ID" ]; then
- echo -e "\e[31;01m!!! An error occured during file import! Please check path.\e[00m"
- else
- echo -e "\e[32m>>> New file added:\e[00m $FILE_ID - $FILE_URI"
- fi
- done
-
- # Files: entire
- get_dir "${THM_BASEURI}${EXO_BASEURI}files/" | grep -v DIGESTS.txt | grep -v '[0-9][0-9]$' | while read f; do basename "$f"; done | while read FILE_URI
- do
- echo "Import file ${THM_BASEURI}${EXO_BASEURI}files/${FILE_URI}"
- FILE_ID=`new_file "${THEME_ID}" "${EXO_ID}" "${THM_BASEURI}${EXO_BASEURI}files/${FILE_URI}"`
- if [ -z "$FILE_ID" ]; then
- echo -e "\e[31;01m!!! An error occured during file import! Please check path.\e[00m"
- else
- echo -e "\e[32m>>> New file added:\e[00m $FILE_ID - $FILE_URI"
- fi
- done
-
-
- LAST=$EXO_ID
- done
- echo
-done
diff --git a/admin/fill_teams.sh b/admin/fill_teams.sh
index 2ff61118..6a1ea7a5 100755
--- a/admin/fill_teams.sh
+++ b/admin/fill_teams.sh
@@ -2,13 +2,14 @@
BASEURL="http://127.0.0.1:8081/admin"
GEN_CERTS=0
+GEN_PASSWD=0
EXTRA_TEAMS=0
CSV_SPLITER=","
-CSV_COL_LASTNAME=1
-CSV_COL_FIRSTNAME=2
-CSV_COL_NICKNAME=3
-CSV_COL_COMPANY=7
-CSV_COL_TEAM=7
+CSV_COL_LASTNAME=2
+CSV_COL_FIRSTNAME=3
+CSV_COL_NICKNAME=5
+CSV_COL_COMPANY=6
+CSV_COL_TEAM=1
usage() {
echo "$0 [options] csv_file"
@@ -16,6 +17,7 @@ usage() {
echo " -S -csv-spliter SEP CSV separator (default: $CSV_SPLITER)"
echo " -e -extra-teams NBS Number of extra teams to generate (default: ${EXTRA_TEAMS})"
echo " -c -generate-certificate Should team certificates be generated? (default: no)"
+ echo " -p -generate-password Should generate team password to teams.pass? (default: no)"
}
# Parse options
@@ -33,6 +35,8 @@ do
shift;;
-c|-generate-certificates)
GEN_CERTS=1;;
+ -p|-generate-password)
+ GEN_PASSWD=1;;
*)
echo "Unknown option '$1'"
usage
@@ -41,8 +45,7 @@ do
shift
done
-[ "$#" -lt 1 ] && { usage; exit 1; }
-PART_FILE="$1"
+[ "$#" -lt 1 ] && [ "${EXTRA_TEAMS}" -eq 0 ] && { usage; exit 1; }
new_team() {
head -n "$1" team-names.txt | tail -1 | sed -E 's/^.*\|\[\[([^|]+\|)?([^|]+)\]\][^|]*\|([A-Fa-f0-9]{1,2})\|([A-Fa-f0-9]{1,2})\|([A-Fa-f0-9]{1,2})\|([0-9]{1,3})\|([0-9]{1,3})\|([0-9]{1,3})\|.*$/\6 \7 \8 \2/' |
@@ -59,7 +62,7 @@ new_team() {
COLOR=$((($R*256 + $G) * 256 + $B))
- curl -s -d "{\"name\": \"$N\",\"color\": $COLOR}" "${BASEURL}/api/teams/"
+ curl -s -d "{\"name\": \"$N\",\"color\": $COLOR}" "${BASEURL}/api/teams"
done | grep -Eo '"id":[0-9]+,' | grep -Eo "[0-9]+"
}
@@ -76,10 +79,30 @@ do
if [ "${GEN_CERTS}" -eq 1 ] && ! curl -s -f "${BASEURL}/api/teams/${TID}/certificate" > /dev/null
then
curl -s -f "${BASEURL}/api/teams/${TID}/certificate/generate"
+ elif [ "${GEN_PASSWD}" -eq 1 ]
+ then
+ TEAMID=$(curl -s -f "${BASEURL}/api/teams/${TID}/" | jq -r .name)
+ PASSWD=$(curl -X POST -s -f "${BASEURL}/api/teams/${TID}/password" | jq -r .password)
+ NP=$(echo "${TEAMID}" | cut -d : -f 1 | sed 's/[[:upper:]]/\l&/g;s/[âáàä]/a/g;s/[êéèë]/e/g')
+ cat >> teams.pass <> htpasswd.ssha <> htpasswd.apr1 < /dev/null
then
curl -s -f "${BASEURL}/api/teams/${TID}/certificate/generate"
+ elif [ "${GEN_PASSWD}" -eq 1 ]
+ then
+ PASSWD=$(curl -X POST -s -f "${BASEURL}/api/teams/${TID}/password" | jq -r .password)
+ NP=$(echo "${TEAMID}" | cut -d : -f 1 | sed 's/[[:upper:]]/\l&/g;s/[âáàä]/a/g;s/[êéèë]/e/g')
+ cat >> teams.pass <> htpasswd.ssha <> htpasswd.apr1 <t\nV33R|(+?$i*'
-
-if [ $# -gt 0 ]
-then
- WHERE=$1
-else
- WHERE="files"
-fi
-
-curl -q -f ${BASEURL}/api/themes/files-bindings | while read l
-do
- FROM=$(echo "$l" | cut -d ";" -f 1)
- DEST=$(echo "$l" | cut -d ";" -f 2)
-
- mkdir -p $(dirname "${WHERE}${DEST}")
-
- wget -O "${WHERE}${DEST}" --user "${CLOUDUSER}" --password "${CLOUDPASS}" "${BASEURI}${FROM}"
-done
diff --git a/admin/index.go b/admin/index.go
index b3a75929..506ff805 100644
--- a/admin/index.go
+++ b/admin/index.go
@@ -4,51 +4,171 @@ const indextpl = `
- Challenge Forensic - Administration
-
+ {{ .title }} - Administration
+
+
+
-
+
-
-
-
+
+
+
+
+
+
+
+
-
-
-
- Équipes
- Thèmes
- Exercices
- Événements
+
-
+
+
+
+
+
+ Loading...
+
+ {{ "{{ staticFilesNeedUpdate }}" }}
+
+
+ Démarrage dans :
+ {{"{{ startIn }}"}} "
+ |
+
+
{{"{{ time.hours | time }}"}}
:
{{"{{ time.minutes | time }}"}}
:
{{"{{ time.seconds | time }}"}}
-
-
-
+
+
-
-
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
`
diff --git a/admin/main.go b/admin/main.go
index a3efc500..06470166 100644
--- a/admin/main.go
+++ b/admin/main.go
@@ -2,80 +2,332 @@ package main
import (
"flag"
- "fmt"
+ "io/fs"
+ "io/ioutil"
"log"
"net/http"
"os"
+ "os/signal"
"path"
"path/filepath"
- "text/template"
+ "strconv"
+ "strings"
+ "syscall"
"srs.epita.fr/fic-server/admin/api"
+ "srs.epita.fr/fic-server/admin/generation"
+ "srs.epita.fr/fic-server/admin/pki"
+ "srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic"
+ "srs.epita.fr/fic-server/settings"
)
-var PKIDir string
-var SubmissionDir string
-
func main() {
- var staticDir string
- var bind = flag.String("bind", "127.0.0.1:8081", "Bind port/socket")
- var dsn = flag.String("dsn", "fic:fic@/fic", "DSN to connect to the MySQL server")
- var baseURL = flag.String("baseurl", "/", "URL prepended to each URL")
- flag.StringVar(&SubmissionDir, "submission", "./submissions/", "Base directory where save submissions")
- flag.StringVar(&PKIDir, "pki", "./pki/", "Base directory where found PKI scripts")
- flag.StringVar(&fic.FilesDir, "files", "./FILES/", "Base directory where found challenges files, local part")
- flag.StringVar(&api.CloudDAVBase, "clouddav", "https://srs.epita.fr/owncloud/remote.php/webdav/FIC 2016",
- "Base directory where found challenges files, cloud part")
- flag.StringVar(&api.CloudUsername, "clouduser", "fic", "Username used to sync")
- flag.StringVar(&api.CloudPassword, "cloudpass", "", "Password used to sync")
+ var err error
+ bind := "127.0.0.1:8081"
+ cloudDAVBase := ""
+ cloudUsername := "fic"
+ cloudPassword := ""
+ localImporterDirectory := ""
+ gitImporterRemote := ""
+ gitImporterBranch := ""
+ localImporterSymlink := false
+ baseURL := "/"
+ checkplugins := sync.CheckPluginList{}
+
+ // Read paremeters from environment
+ if v, exists := os.LookupEnv("FICOIDC_ISSUER"); exists {
+ api.OidcIssuer = v
+ } else if v, exists := os.LookupEnv("FICOIDC_ISSUER_FILE"); exists {
+ fd, err := os.Open(v)
+ if err != nil {
+ log.Fatal("Unable to open FICOIDC_ISSUER_FILE:", err)
+ }
+
+ b, _ := ioutil.ReadAll(fd)
+ api.OidcIssuer = strings.TrimSpace(string(b))
+
+ fd.Close()
+ }
+ if v, exists := os.LookupEnv("FICOIDC_SECRET"); exists {
+ api.OidcSecret = v
+ } else if v, exists := os.LookupEnv("FICOIDC_SECRET_FILE"); exists {
+ fd, err := os.Open(v)
+ if err != nil {
+ log.Fatal("Unable to open FICOIDC_SECRET_FILE:", err)
+ }
+
+ b, _ := ioutil.ReadAll(fd)
+ api.OidcSecret = strings.TrimSpace(string(b))
+
+ fd.Close()
+ }
+ if v, exists := os.LookupEnv("FICCA_PASS"); exists {
+ pki.SetCAPassword(v)
+ } else if v, exists := os.LookupEnv("FICCA_PASS_FILE"); exists {
+ fd, err := os.Open(v)
+ if err != nil {
+ log.Fatal("Unable to open FICCA_PASS_FILE:", err)
+ }
+
+ b, _ := ioutil.ReadAll(fd)
+ pki.SetCAPassword(strings.TrimSpace(string(b)))
+
+ fd.Close()
+ } else {
+ log.Println("WARNING: no password defined for the CA, will use empty password to secure CA private key")
+ log.Println("WARNING: PLEASE DEFINE ENVIRONMENT VARIABLE: FICCA_PASS")
+ }
+ if v, exists := os.LookupEnv("FICCLOUD_URL"); exists {
+ cloudDAVBase = v
+ }
+ if v, exists := os.LookupEnv("FICCLOUD_USER"); exists {
+ cloudUsername = v
+ }
+ if v, exists := os.LookupEnv("FICCLOUD_PASS"); exists {
+ cloudPassword = v
+ } else if v, exists := os.LookupEnv("FICCLOUD_PASS_FILE"); exists {
+ fd, err := os.Open(v)
+ if err != nil {
+ log.Fatal("Unable to open FICCLOUD_PASS_FILE:", err)
+ }
+
+ b, _ := ioutil.ReadAll(fd)
+ cloudPassword = strings.TrimSpace(string(b))
+
+ fd.Close()
+ }
+ if v, exists := os.LookupEnv("FIC_BASEURL"); exists {
+ baseURL = v
+ }
+ if v, exists := os.LookupEnv("FIC_4REAL"); exists {
+ api.IsProductionEnv, err = strconv.ParseBool(v)
+ if err != nil {
+ log.Fatal("Unable to parse FIC_4REAL variable:", err)
+ }
+ }
+ if v, exists := os.LookupEnv("FIC_ADMIN_BIND"); exists {
+ bind = v
+ }
+ if v, exists := os.LookupEnv("FIC_TIMESTAMPCHECK"); exists {
+ api.TimestampCheck = v
+ }
+ if v, exists := os.LookupEnv("FIC_SETTINGS"); exists {
+ settings.SettingsDir = v
+ }
+ if v, exists := os.LookupEnv("FIC_FILES"); exists {
+ fic.FilesDir = v
+ }
+ if v, exists := os.LookupEnv("FIC_SYNC_LOCALIMPORT"); exists {
+ localImporterDirectory = v
+ }
+ if v, exists := os.LookupEnv("FIC_SYNC_LOCALIMPORTSYMLINK"); exists {
+ localImporterSymlink, err = strconv.ParseBool(v)
+ if err != nil {
+ log.Fatal("Unable to parse FIC_SYNC_LOCALIMPORTSYMLINK variable:", err)
+ }
+ }
+ if v, exists := os.LookupEnv("FIC_SYNC_GIT_IMPORT_REMOTE"); exists {
+ gitImporterRemote = v
+ }
+ if v, exists := os.LookupEnv("FIC_SYNC_GIT_BRANCH"); exists {
+ gitImporterBranch = v
+ }
+ if v, exists := os.LookupEnv("FIC_OPTIONALDIGEST"); exists {
+ fic.OptionalDigest, err = strconv.ParseBool(v)
+ if err != nil {
+ log.Fatal("Unable to parse FIC_OPTIONALDIGEST variable:", err)
+ }
+ }
+ if v, exists := os.LookupEnv("FIC_STRONGDIGEST"); exists {
+ fic.StrongDigest, err = strconv.ParseBool(v)
+ if err != nil {
+ log.Fatal("Unable to parse FIC_STRONGDIGEST variable:", err)
+ }
+ }
+
+ // Read parameters from command line
+ flag.StringVar(&bind, "bind", bind, "Bind port/socket")
+ var dsn = flag.String("dsn", fic.DSNGenerator(), "DSN to connect to the MySQL server")
+ flag.StringVar(&baseURL, "baseurl", baseURL, "URL prepended to each URL")
+ flag.StringVar(&api.TimestampCheck, "timestampCheck", api.TimestampCheck, "Path regularly touched by frontend to check time synchronisation")
+ flag.StringVar(&pki.PKIDir, "pki", "./PKI", "Base directory where found PKI scripts")
+ var staticDir = flag.String("static", "", "Directory containing static files (default if not provided: use embedded files)")
+ flag.StringVar(&api.TeamsDir, "teams", "./TEAMS", "Base directory where save teams JSON files")
+ flag.StringVar(&api.DashboardDir, "dashbord", "./DASHBOARD", "Base directory where save public JSON files")
+ flag.StringVar(&settings.SettingsDir, "settings", settings.SettingsDir, "Base directory where load and save settings")
+ flag.StringVar(&fic.FilesDir, "files", fic.FilesDir, "Base directory where found challenges files, local part")
+ flag.StringVar(&generation.GeneratorSocket, "generator", "./GENERATOR/generator.socket", "Path to the generator socket (used to trigger issues.json generations, use an empty string to generate locally)")
+ flag.StringVar(&localImporterDirectory, "localimport", localImporterDirectory,
+ "Base directory where found challenges files to import, local part")
+ flag.BoolVar(&localImporterSymlink, "localimportsymlink", localImporterSymlink,
+ "Copy files or just create symlink?")
+ flag.StringVar(&gitImporterRemote, "git-import-remote", gitImporterRemote,
+ "Remote URL of the git repository to use as synchronization source")
+ flag.StringVar(&gitImporterBranch, "git-branch", gitImporterBranch,
+ "Branch to use in the git repository")
+ flag.StringVar(&cloudDAVBase, "clouddav", cloudDAVBase,
+ "Base directory where found challenges files to import, cloud part")
+ flag.StringVar(&cloudUsername, "clouduser", cloudUsername, "Username used to sync")
+ flag.StringVar(&cloudPassword, "cloudpass", cloudPassword, "Password used to sync")
+ flag.BoolVar(&fic.OptionalDigest, "optionaldigest", fic.OptionalDigest, "Is the digest required when importing files?")
+ flag.BoolVar(&fic.StrongDigest, "strongdigest", fic.StrongDigest, "Are BLAKE2b digests required or is SHA-1 good enough?")
+ flag.BoolVar(&api.IsProductionEnv, "4real", api.IsProductionEnv, "Set this flag when running for a real challenge (it disallows or avoid most of mass user progression deletion)")
+ flag.Var(&checkplugins, "rules-plugins", "List of libraries containing others rules to checks")
+ flag.Var(&sync.RemoteFileDomainWhitelist, "remote-file-domain-whitelist", "List of domains which are allowed to store remote files")
flag.Parse()
log.SetPrefix("[admin] ")
- var err error
- log.Println("Checking paths...")
- if staticDir, err = filepath.Abs(staticDir); err != nil {
- log.Fatal(err)
+ // Instantiate importer
+ if localImporterDirectory != "" && cloudDAVBase != "" {
+ log.Fatal("Cannot have both --clouddav and --localimport defined.")
+ return
+ } else if gitImporterRemote != "" && cloudDAVBase != "" {
+ log.Fatal("Cannot have both --clouddav and --git-import-remote defined.")
+ return
+ } else if gitImporterRemote != "" {
+ sync.GlobalImporter = sync.NewGitImporter(sync.LocalImporter{Base: localImporterDirectory, Symlink: localImporterSymlink}, gitImporterRemote, gitImporterBranch)
+ } else if localImporterDirectory != "" {
+ sync.GlobalImporter = sync.LocalImporter{Base: localImporterDirectory, Symlink: localImporterSymlink}
+ } else if cloudDAVBase != "" {
+ sync.GlobalImporter, _ = sync.NewCloudImporter(cloudDAVBase, cloudUsername, cloudPassword)
}
- if fic.FilesDir, err = filepath.Abs(fic.FilesDir); err != nil {
- log.Fatal(err)
- }
- if PKIDir, err = filepath.Abs(PKIDir); err != nil {
- log.Fatal(err)
- }
- if SubmissionDir, err = filepath.Abs(SubmissionDir); err != nil {
- log.Fatal(err)
- }
- if fic.FilesDir, err = filepath.Abs(fic.FilesDir); err != nil {
- log.Fatal(err)
+ if sync.GlobalImporter != nil {
+ if err := sync.GlobalImporter.Init(); err != nil {
+ log.Fatal("Unable to initialize the importer: ", err.Error())
+ }
+ log.Println("Using", sync.GlobalImporter.Kind())
+
+ challengeinfo, err := sync.GetFileContent(sync.GlobalImporter, settings.ChallengeFile)
+ if err == nil {
+ // Initial distribution of challenge.json
+ if _, err := os.Stat(path.Join(settings.SettingsDir, settings.ChallengeFile)); os.IsNotExist(err) {
+ if fd, err := os.Create(path.Join(settings.SettingsDir, settings.ChallengeFile)); err != nil {
+ log.Fatal("Unable to open SETTINGS/challenge.json:", err)
+ } else {
+ fd.Write([]byte(challengeinfo))
+ err = fd.Close()
+ if err != nil {
+ log.Fatal("Something went wrong during SETTINGS/challenge.json writing:", err)
+ }
+ }
+ }
+
+ if ci, err := settings.ReadChallengeInfo(challengeinfo); err == nil {
+ fic.StandaloneExercicesTheme.Authors = ci.Authors
+ }
+ }
}
+ // Sanitize options
+ log.Println("Checking paths...")
+ if staticDir != nil && *staticDir != "" {
+ if sDir, err := filepath.Abs(*staticDir); err != nil {
+ log.Fatal(err)
+ } else {
+ log.Println("Serving pages from", sDir)
+ staticFS = http.Dir(sDir)
+ sync.DeepReportPath = path.Join(sDir, sync.DeepReportPath)
+ }
+ } else {
+ sub, err := fs.Sub(assets, "static")
+ if err != nil {
+ log.Fatal("Unable to cd to static/ directory:", err)
+ }
+ log.Println("Serving pages from memory.")
+ staticFS = http.FS(sub)
+
+ sync.DeepReportPath = path.Join("SYNC", sync.DeepReportPath)
+ if _, err := os.Stat("SYNC"); os.IsNotExist(err) {
+ os.MkdirAll("SYNC", 0751)
+ }
+ }
+ if fic.FilesDir, err = filepath.Abs(fic.FilesDir); err != nil {
+ log.Fatal(err)
+ }
+ if pki.PKIDir, err = filepath.Abs(pki.PKIDir); err != nil {
+ log.Fatal(err)
+ }
+ if api.DashboardDir, err = filepath.Abs(api.DashboardDir); err != nil {
+ log.Fatal(err)
+ }
+ if api.TeamsDir, err = filepath.Abs(api.TeamsDir); err != nil {
+ log.Fatal(err)
+ }
+ if api.TimestampCheck, err = filepath.Abs(api.TimestampCheck); err != nil {
+ log.Fatal(err)
+ }
+ if settings.SettingsDir, err = filepath.Abs(settings.SettingsDir); err != nil {
+ log.Fatal(err)
+ }
+ if baseURL != "/" {
+ baseURL = path.Clean(baseURL)
+ } else {
+ baseURL = ""
+ }
+
+ // Creating minimal directories structure
+ os.MkdirAll(fic.FilesDir, 0751)
+ os.MkdirAll(pki.PKIDir, 0711)
+ os.MkdirAll(api.TeamsDir, 0751)
+ os.MkdirAll(api.DashboardDir, 0751)
+ os.MkdirAll(settings.SettingsDir, 0751)
+
+ // Load rules plugins
+ for _, p := range checkplugins {
+ if err := sync.LoadChecksPlugin(p); err != nil {
+ log.Fatalf("Unable to load rule plugin %q: %s", p, err.Error())
+ } else {
+ log.Printf("Rules plugin %q successfully loaded", p)
+ }
+ }
+
+ // Initialize settings and load them
+ if !settings.ExistsSettings(path.Join(settings.SettingsDir, settings.SettingsFile)) {
+ if err = api.ResetSettings(); err != nil {
+ log.Fatal("Unable to initialize settings.json:", err)
+ }
+ }
+ var config *settings.Settings
+ if config, err = settings.ReadSettings(path.Join(settings.SettingsDir, settings.SettingsFile)); err != nil {
+ log.Fatal("Unable to read settings.json:", err)
+ } else {
+ api.ApplySettings(config)
+ }
+
+ // Initialize dashboard presets
+ if err = api.InitDashboardPresets(api.DashboardDir); err != nil {
+ log.Println("Unable to initialize dashboards presets:", err)
+ }
+
+ // Database connection
log.Println("Opening database...")
- if err := fic.DBInit(fmt.Sprintf("%s?parseTime=true", *dsn)); err != nil {
+ if err = fic.DBInit(*dsn); err != nil {
log.Fatal("Cannot open the database: ", err)
}
defer fic.DBClose()
log.Println("Creating database...")
- if err := fic.DBCreate(); err != nil {
+ if err = fic.DBCreate(); err != nil {
log.Fatal("Cannot create database: ", err)
}
- log.Println("Changing base url...")
- if file, err := os.OpenFile(path.Join(staticDir, "index.html"), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(0644)); err != nil {
- log.Println("Unable to open index.html: ", err)
- } else if indexTmpl, err := template.New("index").Parse(indextpl); err != nil {
- log.Println("Cannot create template: ", err)
- } else if err := indexTmpl.Execute(file, map[string]string{"urlbase": path.Clean(path.Join(*baseURL, "nuke"))[:len(path.Clean(path.Join(*baseURL, "nuke"))) - 4]}); err != nil {
- log.Println("An error occurs during template execution: ", err)
- }
+ // Update base URL on main page
+ log.Println("Changing base URL to", baseURL+"/", "...")
+ genIndex(baseURL)
+ // Prepare graceful shutdown
+ interrupt := make(chan os.Signal, 1)
+ signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM)
- os.Chdir(PKIDir)
+ app := NewApp(config, baseURL, bind)
+ go app.Start()
- log.Println(fmt.Sprintf("Ready, listening on %s", *bind))
- if err := http.ListenAndServe(*bind, http.StripPrefix(*baseURL, api.Router())); err != nil {
- log.Fatal("Unable to listen and serve: ", err)
- }
+ // Wait shutdown signal
+ <-interrupt
+
+ log.Print("The service is shutting down...")
+ app.Stop()
+ log.Println("done")
}
diff --git a/admin/pki/CA.sh b/admin/pki/CA.sh
deleted file mode 100755
index dd861959..00000000
--- a/admin/pki/CA.sh
+++ /dev/null
@@ -1,284 +0,0 @@
-#!/bin/bash
-
-cd $(dirname "$0")
-
-if [ -z "${PKI_BASEDIR}" ]; then
- PKI_BASEDIR=$(dirname `pwd`) # equivalent to $(realpath `pwd`/..
-fi
-
-PKI_DIR=${PKI_BASEDIR}/PKI
-SHARED_DIR=${PKI_DIR}/shared
-OPENSSL_CONF=`pwd`/openssl.cnf
-
-CAKEY=${PKI_DIR}/private/cakey.key
-CAREQ=${PKI_DIR}/careq.csr
-CACRT=${SHARED_DIR}/cacert.crt
-CADER=${SHARED_DIR}/cacert.der
-
-SRVKEY=${SHARED_DIR}/server.key
-SRVREQ=${SHARED_DIR}/server.csr
-SRVCRT=${SHARED_DIR}/server.crt
-
-# Generate certificates valid for:
-DAYS=2
-STARTDATE=160125000000Z
-ENDDATE=160126235959Z
-VALIDITY="-startdate ${STARTDATE} -enddate ${ENDDATE}"
-#VALIDITY="-days ${DAYS}"
-
-if [ -z "$PS1" ]
-then
- GREEN="\033[1;32m"
- RED="\033[1;31m"
- COLOR_RST="\033[0m"
- BOLD=""
- END_BOLD=""
- ECHO_OPTS="-e"
-else
- GREEN="
"
- RED=""
- COLOR_RST=" "
- BOLD=""
- END_BOLD=" "
- ECHO_OPTS=""
-fi
-
-usage()
-{
- echo "Usage: $0 (-newca|-newserver IP/URL|-revokeserver|-newclient NAME|-revoke NAME|-gencrl)"
- exit 1
-}
-
-clean()
-{
- if [ "$1" = "ca" ]; then
- rm -rf ${PKI_DIR}/* ${SHARED_DIR}/*
- mkdir -p ${PKI_DIR}/certs ${PKI_DIR}/crl ${PKI_DIR}/newcerts \
- ${PKI_DIR}/private ${PKI_DIR}/pkcs ${SHARED_DIR}
- echo "01" > ${PKI_DIR}/crlnumber
- elif [ "$1" = "client" ]; then
- rm -rf ${PKI_DIR}/${2}.key ${PKI_DIR}/${2}.csr
- fi
- rm -rf $OUTPUT
-}
-
-gen_crl()
-{
- echo $ECHO_OPTS "${GREEN}Generate shared/crl.pem${COLOR_RST}"
- if ! openssl ca -config ${OPENSSL_CONF} -gencrl -out ${SHARED_DIR}/crl.pem > $OUTPUT 2>&1
- then
- echo $ECHO_OPTS "${RED}Generate shared/crl.pem failed"
- cat $OUTPUT
- exit 5
- fi
-}
-
-[ $# -lt 1 ] && usage
-
-OUTPUT=$(mktemp)
-
-case $1 in
- "-newca" )
- echo $ECHO_OPTS "${GREEN}Create the directories, take care this will delete the old directories ${COLOR_RST}"
-
- clean "ca"
- touch ${PKI_DIR}/index.txt
-
- ESCAPED=$(echo "${PKI_DIR}" | sed 's/[\/\.]/\\&/g')
-
- echo $ECHO_OPTS "${GREEN}Making CA key and csr${COLOR_RST}"
- sed -i 's/=.*#COMMONNAME/= FIC CA #COMMONNAME/' $OPENSSL_CONF
- sed -i "s/=.*#DIR/= ${ESCAPED} #DIR/" $OPENSSL_CONF
- sed -i "s/=.*#DAYS/= ${DAYS} #DAYS/" $OPENSSL_CONF
-
- type pwgen > /dev/null
- if [ $? -ne 0 ]; then
- echo "command not found: pwgen"
- exit 5
- fi
-
- pass=`pwgen -n -B -y 12 1`
- if ! openssl req -batch -new -keyout ${CAKEY} \
- -out ${CAREQ} -passout pass:$pass \
- -config ${OPENSSL_CONF} -extensions CORE_CA > $OUTPUT 2>&1
- then
- cat $OUTPUT
- clean "ca"
- exit 4
- fi
-
- # This line deleted the passphase for the FIC 2014 automatisation
- if ! openssl rsa -passin pass:$pass -in ${CAKEY} \
- -out ${CAKEY} > $OUTPUT 2>&1
- then
- cat $OUTPUT
- clean "ca"
- exit 4
- fi
-
- echo $ECHO_OPTS "${GREEN}Self signes the CA certificate${COLOR_RST}"
- if ! openssl ca -batch -create_serial -out ${CACRT} \
- ${VALIDITY} -keyfile ${CAKEY} \
- -selfsign -extensions CORE_CA -config ${OPENSSL_CONF} \
- -infiles ${CAREQ} > $OUTPUT 2>&1
- then
- cat $OUTPUT
- clean "ca"
- exit 4
- fi
-
- echo $ECHO_OPTS "${GREEN}Generate DER format${COLOR_RST}"
- openssl x509 -in ${CACRT} -inform PEM -out ${CADER} -outform DER
- ;;
-
- "-newserver" )
- if [ $# -lt 2 ]; then
- echo "Give as first argument the production IP or the domain that this certificat will cover."
- echo "eg.: $0 -newserver 10.42.23.69"
- echo " $0 -newserver fic.srs.epita.fr"
- exit 1
- fi
-
- echo $ECHO_OPTS "${GREEN}Making the Server key and cert${COLOR_RST}"
- if ! [ -f ${CAKEY} ]; then
- echo $ECHO_OPTS "${RED}Can not found the CA's key${COLOR_RST}"
- exit 2
- fi
- sed -i "s/=.*#COMMONNAME/=$2#COMMONNAME/" $OPENSSL_CONF
- sed -i "s/=.*#DAYS/= ${DAYS} #DAYS/" $OPENSSL_CONF
- if ! openssl req -batch -new -keyout ${SRVKEY} -out ${SRVREQ} \
- -days ${DAYS} -config ${OPENSSL_CONF} -extensions SERVER_SSL > $OUTPUT 2>&1
- then
- cat $OUTPUT
- exit 4
- fi
- echo $ECHO_OPTS "${GREEN}Signing the Server crt${COLOR_RST}"
- if ! openssl ca -policy policy_match -config ${OPENSSL_CONF} \
- -out ${SRVCRT} -extensions SERVER_SSL -infiles ${SRVREQ}
- then
- echo $ECHO_OPTS "${RED}Signing failed for new server${COLOR_RST}"
- rm -f ${SRVKEY} ${SRVREQ} ${SRVCRT}
- cat $OUTPUT
- exit 3
- else
- rm ${SRVREQ}
- echo $ECHO_OPTS "${GREEN}Signed certificate is in ${SRVCRT}${COLOR_RST}"
- fi
- ;;
-
- "-revokeserver" )
- echo $ECHO_OPTS "${GREEN}Revocate server certificate${COLOR_RST}"
- if ! [ -f ${CAKEY} ]; then
- echo $ECHO_OPTS "${RED}Can not found the CA's key${COLOR_RST}"
- exit 2
- fi
- if ! openssl ca -revoke ${SRVCRT} -config ${OPENSSL_CONF} \
- -keyfile ${CAKEY} -cert ${CACRT} > $OUTPUT 2>&1
- then
- echo $ECHO_OPTS "${RED}Server certificate revocation failed${COLOR_RST}"
- cat $OUTPUT
- exit 4
- fi
- rm ${SRVKEY} ${SRVCRT}
-
- gen_crl
- ;;
-
- "-newclient" )
- if [ $# -ne 2 ]; then
- echo "Usage: $0 -newclient NAME"
- exit 1
- fi
-
- CLTNAM=$2
- CLTREQ=${PKI_DIR}/${CLTNAM}.csr
- CLTCRT=${PKI_DIR}/certs/${CLTNAM}.crt
- CLTKEY=${PKI_DIR}/${CLTNAM}.key
- CLTP12=${PKI_DIR}/pkcs/${CLTNAM}.p12
-
- echo "=============================================================="
- echo $ECHO_OPTS "${GREEN}Making the client key and csr of ${BOLD}${2}${END_BOLD}${COLOR_RST}"
-
- ESCAPED=$(echo "${PKI_DIR}" | sed 's/[\/\.]/\\&/g')
- sed -i "s/=.*#DIR/= ${ESCAPED} #DIR/" $OPENSSL_CONF
- sed -i "s/=.*#DAYS/= ${DAYS} #DAYS/" $OPENSSL_CONF
-
- if ! [ -f ${CAKEY} ]; then
- echo $ECHO_OPTS "${RED}Can not found the CA's key${COLOR_RST}"
- exit 2
- fi
- sed -i "s/=.*#COMMONNAME/= $2#COMMONNAME/" $OPENSSL_CONF
-
- type pwgen > /dev/null
- if [ $? -ne 0 ]; then
- echo "command not found: pwgen"
- exit 5
- fi
-
- pass=`pwgen -n -B -y 12 1`
-
- if ! openssl req -batch -new -keyout "${CLTKEY}" -out "${CLTREQ}" \
- -config ${OPENSSL_CONF} -passout pass:$pass -days ${DAYS} -extensions CLIENT_SSL > $OUTPUT 2>&1
- then
- cat $OUTPUT
- clean "client" ${CLTNAM}
- exit 4
- fi
-
- echo $ECHO_OPTS "${GREEN}Signing the Client crt${COLOR_RST}"
- if ! openssl ca -batch -policy policy_match -out "${CLTCRT}" \
- -config ${OPENSSL_CONF} -extensions CLIENT_SSL -infiles "${CLTREQ}" > $OUTPUT 2>&1
- then
- echo $ECHO_OPTS "${RED}Signing failed for $2 ${COLOR_RST}"
- cat $OUTPUT
- clean "client" ${CLTNAM}
- exit 3
- fi
- echo $ECHO_OPTS "${GREEN}Export the Client files to pkcs12${COLOR_RST}"
- if ! openssl pkcs12 -export -inkey "${CLTKEY}" -in "${CLTCRT}" -name ${2} \
- -passin pass:$pass -out "${CLTP12}" \
- -passout pass:$pass > $OUTPUT 2>&1
- then
- echo $ECHO_OPTS "${RED}pkcs12 export failed for ${BOLD}$2${END_BOLD}${COLOR_RST}"
- cat $OUTPUT
- clean "client" ${CLTNAM}
- exit 4
- else
- echo $ECHO_OPTS "Exported pkcs12 file is ${CLTP12}"
- fi
- echo "$CLTNAM:$pass" >> ${PKI_DIR}/teams.pass
- echo "$CLTNAM:$pass"
- clean "client" ${CLTNAM}
- ;;
-
- "-revoke" )
- if [ $# -ne 2 ]; then
- echo "Usage: $0 -revoke NAME"
- exit 1
- fi
-
- CLTNAM=$2
- CLTCRT=${PKI_DIR}/certs/${CLTNAM}.crt
- CLTP12=${PKI_DIR}/pkcs/${CLTNAM}.p12
-
- echo $ECHO_OPTS "${GREEN}Revocate ${BOLD}${CLTNAM}${END_BOLD}${COLOR_RST}"
- if ! openssl ca -revoke "${CLTCRT}" -config "${OPENSSL_CONF}" \
- -keyfile "${CAKEY}" -cert "${CACRT}" > $OUTPUT 2>&1
- then
- echo $ECHO_OPTS "${RED}Revocation failed for ${BOLD}${CLTNAM}${END_BOLD}${COLOR_RST}"
- cat $OUTPUT
- exit 4
- fi
- rm "${CLTCRT}" "${CLTP12}"
-
- gen_crl
- ;;
-
- "-gencrl" )
- gen_crl
- ;;
-
- * )
- usage
- ;;
-esac
diff --git a/admin/pki/ca.go b/admin/pki/ca.go
new file mode 100644
index 00000000..dfbb9f1f
--- /dev/null
+++ b/admin/pki/ca.go
@@ -0,0 +1,133 @@
+package pki
+
+import (
+ "crypto/ecdsa"
+ "crypto/rand"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/pem"
+ "errors"
+ "io/ioutil"
+ "math/big"
+ "os"
+ "path"
+ "time"
+)
+
+var passwordCA string
+
+func SetCAPassword(pass string) {
+ passwordCA = pass
+}
+
+func CACertPath() string {
+ return path.Join(PKIDir, "shared", "ca.pem")
+}
+
+func CAPrivkeyPath() string {
+ return path.Join(PKIDir, "ca.key")
+}
+
+func GenerateCA(notBefore time.Time, notAfter time.Time) error {
+ ca := &x509.Certificate{
+ SerialNumber: big.NewInt(0),
+ Subject: pkix.Name{
+ Organization: []string{"EPITA"},
+ OrganizationalUnit: []string{"SRS laboratory"},
+ Country: []string{"FR"},
+ Locality: []string{"Paris"},
+ CommonName: "FIC CA",
+ },
+ NotBefore: notBefore,
+ NotAfter: notAfter,
+ IsCA: true,
+ ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
+ KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
+ BasicConstraintsValid: true,
+ }
+
+ // Ensure directories exists
+ os.Mkdir(PKIDir, 0751)
+ os.Mkdir(path.Join(PKIDir, "shared"), 0751)
+
+ pub, priv, err := GeneratePrivKey()
+ if err != nil {
+ return err
+ }
+
+ ca_b, err := x509.CreateCertificate(rand.Reader, ca, ca, pub, priv)
+ if err != nil {
+ return err
+ }
+
+ // Save certificate to file
+ if err := saveCertificate(CACertPath(), ca_b); err != nil {
+ return err
+ }
+
+ // Save private key to file
+ if err := savePrivateKeyEncrypted(CAPrivkeyPath(), priv, passwordCA); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func LoadCA() (priv ecdsa.PrivateKey, ca x509.Certificate, err error) {
+ // Load certificate
+ if fd, errr := os.Open(CACertPath()); errr != nil {
+ return priv, ca, errr
+ } else {
+ defer fd.Close()
+ if cert, errr := ioutil.ReadAll(fd); errr != nil {
+ return priv, ca, errr
+ } else {
+ block, _ := pem.Decode(cert)
+ if block == nil || block.Type != "CERTIFICATE" {
+ return priv, ca, errors.New("failed to decode PEM block containing certificate")
+ }
+ if catmp, errr := x509.ParseCertificate(block.Bytes); errr != nil {
+ return priv, ca, errr
+ } else if catmp == nil {
+ return priv, ca, errors.New("failed to parse certificate")
+ } else {
+ ca = *catmp
+ }
+ }
+ }
+
+ // Load private key
+ if fd, errr := os.Open(CAPrivkeyPath()); errr != nil {
+ return priv, ca, errr
+ } else {
+ defer fd.Close()
+ if privkey, errr := ioutil.ReadAll(fd); errr != nil {
+ return priv, ca, errr
+ } else {
+ block, _ := pem.Decode(privkey)
+ if block == nil || block.Type != "EC PRIVATE KEY" {
+ return priv, ca, errors.New("failed to decode PEM block containing EC private key")
+ }
+
+ var decrypted_der []byte
+ if x509.IsEncryptedPEMBlock(block) {
+ decrypted_der, err = x509.DecryptPEMBlock(block, []byte(passwordCA))
+ if err != nil {
+ return
+ }
+ } else {
+ decrypted_der = block.Bytes
+ }
+
+ if tmppriv, errr := x509.ParseECPrivateKey(decrypted_der); errr != nil {
+ return priv, ca, errr
+ } else if tmppriv == nil {
+ return priv, ca, errors.New("failed to parse private key")
+ } else {
+ priv = *tmppriv
+ }
+ }
+ }
+
+ return
+}
diff --git a/admin/pki/client.go b/admin/pki/client.go
new file mode 100644
index 00000000..a64ebc4b
--- /dev/null
+++ b/admin/pki/client.go
@@ -0,0 +1,84 @@
+package pki
+
+import (
+ "crypto/ecdsa"
+ "crypto/rand"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "fmt"
+ "math"
+ "math/big"
+ "os"
+ "os/exec"
+ "path"
+ "time"
+)
+
+func ClientCertificatePath(serial uint64) string {
+ return path.Join(PKIDir, fmt.Sprintf("%0[2]*[1]X", serial, int(math.Ceil(math.Log2(float64(serial))/8)*2)), "cert.pem")
+}
+
+func ClientPrivkeyPath(serial uint64) string {
+ return path.Join(PKIDir, fmt.Sprintf("%0[2]*[1]X", serial, int(math.Ceil(math.Log2(float64(serial))/8)*2)), "privkey.pem")
+}
+
+func ClientP12Path(serial uint64) string {
+ return path.Join(PKIDir, fmt.Sprintf("%0[2]*[1]X", serial, int(math.Ceil(math.Log2(float64(serial))/8)*2)), "team.p12")
+}
+
+func GenerateClient(serial uint64, notBefore time.Time, notAfter time.Time, parent_cert *x509.Certificate, parent_priv *ecdsa.PrivateKey) error {
+ var certid big.Int
+ certid.SetUint64(serial)
+ client := &x509.Certificate{
+ SerialNumber: &certid,
+ Subject: pkix.Name{
+ Organization: []string{"EPITA"},
+ OrganizationalUnit: []string{"SRS laboratory"},
+ Country: []string{"FR"},
+ Locality: []string{"Paris"},
+ CommonName: fmt.Sprintf("TEAM-%0[2]*[1]X", serial, int(math.Ceil(math.Log2(float64(serial))/8)*2)),
+ },
+ NotBefore: notBefore,
+ NotAfter: notAfter,
+ IsCA: false,
+ ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
+ KeyUsage: x509.KeyUsageDigitalSignature,
+ BasicConstraintsValid: true,
+ }
+
+ pub, priv, err := GeneratePrivKey()
+ if err != nil {
+ return err
+ }
+
+ client_b, err := x509.CreateCertificate(rand.Reader, client, parent_cert, pub, parent_priv)
+ if err != nil {
+ return err
+ }
+
+ // Create intermediate directory
+ os.MkdirAll(path.Join(PKIDir, fmt.Sprintf("%0[2]*[1]X", serial, int(math.Ceil(math.Log2(float64(serial))/8)*2))), 0777)
+
+ // Save certificate to file
+ if err := saveCertificate(ClientCertificatePath(serial), client_b); err != nil {
+ return err
+ }
+
+ // Save private key to file
+ if err := savePrivateKey(ClientPrivkeyPath(serial), priv); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func WriteP12(serial uint64, password string) error {
+ cmd := exec.Command("/usr/bin/openssl", "pkcs12", "-export",
+ "-inkey", ClientPrivkeyPath(serial),
+ "-in", ClientCertificatePath(serial),
+ "-name", fmt.Sprintf("TEAM-%0[2]*[1]X", serial, int(math.Ceil(math.Log2(float64(serial))/8)*2)),
+ "-passout", "pass:" + password,
+ "-out", ClientP12Path(serial))
+
+ return cmd.Run()
+}
diff --git a/admin/pki/common.go b/admin/pki/common.go
new file mode 100644
index 00000000..4310084a
--- /dev/null
+++ b/admin/pki/common.go
@@ -0,0 +1,62 @@
+package pki
+
+import (
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/x509"
+ "encoding/pem"
+ "os"
+)
+
+var PKIDir string
+
+func GeneratePrivKey() (pub *ecdsa.PublicKey, priv *ecdsa.PrivateKey, err error) {
+ if priv, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader); err == nil {
+ pub = &priv.PublicKey
+ }
+ return
+}
+
+func saveCertificate(path string, cert []byte) error {
+ if certOut, err := os.Create(path); err != nil {
+ return err
+ } else {
+ pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: cert})
+ certOut.Close()
+ }
+ return nil
+}
+
+func savePrivateKey(path string, private *ecdsa.PrivateKey) error {
+ if keyOut, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600); err != nil {
+ return err
+ } else if key_b, err := x509.MarshalECPrivateKey(private); err != nil {
+ return err
+ } else {
+ pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: key_b})
+ keyOut.Close()
+ }
+ return nil
+}
+
+func savePrivateKeyEncrypted(path string, private *ecdsa.PrivateKey, password string) error {
+ if password == "" {
+ return savePrivateKey(path, private)
+ }
+
+ if keyOut, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600); err != nil {
+ return err
+ } else {
+ defer keyOut.Close()
+
+ if key_b, err := x509.MarshalECPrivateKey(private); err != nil {
+ return err
+ } else if key_c, err := x509.EncryptPEMBlock(rand.Reader, "EC PRIVATE KEY", key_b, []byte(password), x509.PEMCipherAES256); err != nil {
+ return err
+ } else {
+ pem.Encode(keyOut, key_c)
+ }
+ }
+ return nil
+}
diff --git a/admin/pki/openssl.cnf b/admin/pki/openssl.cnf
deleted file mode 100644
index 495674f9..00000000
--- a/admin/pki/openssl.cnf
+++ /dev/null
@@ -1,197 +0,0 @@
-#
-# OpenSSL example configuration file.
-# This is mostly being used for generation of certificate requests.
-#
-
-# This definition stops the following lines choking if HOME isn't
-# defined.
-HOME = .
-RANDFILE = $ENV::HOME/.rnd
-
-# Extra OBJECT IDENTIFIER info:
-#oid_file = $ENV::HOME/.oid
-oid_section = new_oids
-
-# To use this configuration file with the "-extfile" option of the
-# "openssl x509" utility, name here the section containing the
-# X.509v3 extensions to use:
-# extensions =
-# (Alternatively, use a configuration file that has only
-# X.509v3 extensions in its main [= default] section.)
-
-[ new_oids ]
-
-# We can add new OIDs in here for use by 'ca', 'req' and 'ts'.
-# Add a simple OID like this:
-# testoid1=1.2.3.4
-# Or use config file substitution like this:
-# testoid2=${testoid1}.5.6
-
-# Policies used by the TSA examples.
-tsa_policy1 = 1.2.3.4.1
-tsa_policy2 = 1.2.3.4.5.6
-tsa_policy3 = 1.2.3.4.5.7
-
-####################################################################
-[ ca ]
-default_ca = CA_default # The default ca section
-
-####################################################################
-[ CA_default ]
-
-dir = /home/nemunaire/workspace/gowks/src/srs.epita.fr/fic-server/admin/PKI #DIR # Where everything is kept
-certs = $dir/certs # Where the issued certs are kept
-crl_dir = $dir/crl # Where the issued crl are kept
-database = $dir/index.txt # database index file.
-#unique_subject = no # Set to 'no' to allow creation of
- # several ctificates with same subject.
-new_certs_dir = $dir/newcerts # default place for new certs.
-
-certificate = $dir/shared/cacert.crt # The CA certificate
-serial = $dir/serial # The current serial number
-crlnumber = $dir/crlnumber # the current crl number
- # must be commented out to leave a V1 CRL
-crl = $dir/shared/crl.pem # The current CRL
-private_key = $dir/private/cakey.key # The private key
-RANDFILE = $dir/private/.rand # private random number file
-
-# Comment out the following two lines for the "traditional"
-# (and highly broken) format.
-name_opt = ca_default # Subject Name options
-cert_opt = ca_default # Certificate field options
-
-# Extension copying option: use with caution.
-# copy_extensions = copy
-
-# Extensions to add to a CRL. Note: Netscape communicator chokes on V2 CRLs
-# so this is commented out by default to leave a V1 CRL.
-# crlnumber must also be commented out to leave a V1 CRL.
-# crl_extensions = crl_ext
-
-default_days = 2 #DAYS
-default_crl_days= 1 # how long before next CRL
-default_md = default # use public key default MD
-preserve = no # keep passed DN ordering
-
-# A few difference way of specifying how similar the request should look
-# For type CA, the listed attributes must be the same, and the optional
-# and supplied fields are just that :-)
-policy = policy_match
-
-# For the CA policy
-[ policy_match ]
-countryName = match
-stateOrProvinceName = match
-organizationName = match
-organizationalUnitName = optional
-commonName = supplied
-emailAddress = optional
-
-# For the 'anything' policy
-# At this point in time, you must list all acceptable 'object'
-# types.
-[ policy_anything ]
-countryName = optional
-stateOrProvinceName = optional
-localityName = optional
-organizationName = optional
-organizationalUnitName = optional
-commonName = supplied
-emailAddress = optional
-
-####################################################################
-[ req ]
-default_bits = 2048
-default_keyfile = privkey.pem
-distinguished_name = req_distinguished_name
-attributes = req_attributes
-x509_extensions = v3_ca # The extentions to add to the self signed cert
-
-# Passwords for private keys if not present they will be prompted for
-# input_password = secret
-# output_password = secret
-
-# This sets a mask for permitted string types. There are several options.
-# default: PrintableString, T61String, BMPString.
-# pkix : PrintableString, BMPString (PKIX recommendation before 2004)
-# utf8only: only UTF8Strings (PKIX recommendation after 2004).
-# nombstr : PrintableString, T61String (no BMPStrings or UTF8Strings).
-# MASK:XXXX a literal mask value.
-# WARNING: ancient versions of Netscape crash on BMPStrings or UTF8Strings.
-string_mask = utf8only
-
-# req_extensions = v3_req # The extensions to add to a certificate request
-
-[ req_distinguished_name ]
-countryName = Country Name (2 letter code)
-countryName_default = FR
-countryName_min = 2
-countryName_max = 2
-
-stateOrProvinceName = State or Province Name (full name)
-stateOrProvinceName_default = France
-
-localityName = Locality Name (eg, city)
-localityName_default = Paris
-
-0.organizationName = Organization Name (eg, company)
-0.organizationName_default = Epita
-
-# we can do this but it is not needed normally :-)
-#1.organizationName = Second Organization Name (eg, company)
-#1.organizationName_default = World Wide Web Pty Ltd
-
-organizationalUnitName = Organizational Unit Name (eg, section)
-organizationalUnitName_default = SRS
-
-commonName = Common Name (e.g. server FQDN or YOUR name)
-commonName_default = Acier#COMMONNAME
-commonName_max = 64
-
-emailAddress = Email Address
-emailAddress_max = 64
-emailAddress_default = root@srs.epita.fr
-
-# SET-ex3 = SET extension number 3
-
-[ req_attributes ]
-challengePassword = A challenge password
-challengePassword_min = 4
-challengePassword_max = 20
-
-unstructuredName = An optional company name
-
-[CORE_CA]
-nsComment = "FIC CA"
-basicConstraints = critical,CA:TRUE,pathlen:1
-subjectKeyIdentifier = hash
-authorityKeyIdentifier = keyid,issuer:always
-issuerAltName = issuer:copy
-keyUsage = keyCertSign, cRLSign
-nsCertType = sslCA
-subjectKeyIdentifier=hash
-authorityKeyIdentifier=keyid:always,issuer
-
-[SERVER_SSL]
-nsComment = "FIC Server"
-subjectKeyIdentifier = hash
-authorityKeyIdentifier = keyid,issuer:always
-issuerAltName = issuer:copy
-basicConstraints = critical,CA:FALSE
-keyUsage = digitalSignature, keyEncipherment
-nsCertType = server
-extendedKeyUsage = serverAuth
-subjectKeyIdentifier=hash
-authorityKeyIdentifier=keyid:always,issuer
-
-[CLIENT_SSL]
-nsComment = "FIC Client"
-subjectKeyIdentifier = hash
-authorityKeyIdentifier = keyid,issuer:always
-issuerAltName = issuer:copy
-basicConstraints = critical,CA:FALSE
-keyUsage = digitalSignature, nonRepudiation
-nsCertType = client
-extendedKeyUsage = clientAuth
-subjectKeyIdentifier=hash
-authorityKeyIdentifier=keyid:always,issuer
diff --git a/admin/pki/team.go b/admin/pki/team.go
new file mode 100644
index 00000000..8ea91535
--- /dev/null
+++ b/admin/pki/team.go
@@ -0,0 +1,79 @@
+package pki
+
+import (
+ "fmt"
+ "io/ioutil"
+ "math"
+ "os"
+ "path"
+ "strconv"
+ "strings"
+)
+
+const SymlinkPrefix = "_AUTH_ID_"
+
+func GetCertificateAssociation(serial uint64) string {
+ return fmt.Sprintf(SymlinkPrefix+"%0[2]*[1]X", serial, int(math.Ceil(math.Log2(float64(serial))/8)*2))
+}
+
+func GetAssociation(dirname string) (assocs string, err error) {
+ return os.Readlink(dirname)
+}
+
+func GetAssociations(dirname string) (assocs []string, err error) {
+ if ds, errr := ioutil.ReadDir(dirname); errr != nil {
+ return nil, errr
+ } else {
+ for _, d := range ds {
+ if d.Mode()&os.ModeSymlink == os.ModeSymlink {
+ assocs = append(assocs, d.Name())
+ }
+ }
+ return
+ }
+}
+
+func GetTeamSerials(dirname string, id_team int64) (serials []uint64, err error) {
+ // As futher comparaisons will be made with strings, convert it only one time
+ str_tid := fmt.Sprintf("%d", id_team)
+
+ var assocs []string
+ if assocs, err = GetAssociations(dirname); err != nil {
+ return
+ } else {
+ for _, assoc := range assocs {
+ var tid string
+ if tid, err = os.Readlink(path.Join(dirname, assoc)); err == nil && tid == str_tid && strings.HasPrefix(assoc, SymlinkPrefix) {
+ if serial, err := strconv.ParseUint(assoc[9:], 16, 64); err == nil {
+ serials = append(serials, serial)
+ }
+ }
+ }
+ }
+ return
+}
+
+func GetTeamAssociations(dirname string, id_team int64) (teamAssocs []string, err error) {
+ // As futher comparaisons will be made with strings, convert it only one time
+ str_tid := fmt.Sprintf("%d", id_team)
+
+ var assocs []string
+ if assocs, err = GetAssociations(dirname); err != nil {
+ return
+ } else {
+ for _, assoc := range assocs {
+ var tid string
+ if tid, err = os.Readlink(path.Join(dirname, assoc)); err == nil && tid == str_tid && !strings.HasPrefix(assoc, SymlinkPrefix) {
+ teamAssocs = append(teamAssocs, assoc)
+ }
+ }
+ }
+ return
+}
+
+func DeleteTeamAssociation(dirname string, assoc string) error {
+ if err := os.Remove(path.Join(dirname, assoc)); err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/admin/static.go b/admin/static.go
index 6c7b7cc6..23ad6da6 100644
--- a/admin/static.go
+++ b/admin/static.go
@@ -1,18 +1,160 @@
package main
import (
+ "bytes"
+ "embed"
+ "errors"
+ "log"
"net/http"
+ "os"
"path"
+ "strings"
+ "text/template"
+
+ "srs.epita.fr/fic-server/admin/api"
+ "srs.epita.fr/fic-server/admin/sync"
+ "srs.epita.fr/fic-server/libfic"
+ "srs.epita.fr/fic-server/settings"
+
+ "github.com/gin-gonic/gin"
)
-type staticRouting struct {
- StaticDir string
+//go:embed static
+
+var assets embed.FS
+
+var indexPage []byte
+
+func genIndex(baseURL string) {
+ tplcfg := map[string]string{
+ "logo": "img/logo.png",
+ "title": "Challenge",
+ "urlbase": path.Clean(path.Join(baseURL+"/", "nuke"))[:len(path.Clean(path.Join(baseURL+"/", "nuke")))-4],
+ }
+
+ ci, err := api.GetChallengeInfo()
+ if err == nil && ci != nil {
+ tplcfg["title"] = ci.Title
+ if len(ci.MainLogo) > 0 {
+ tplcfg["logo"] = "/files/logo/" + path.Base(ci.MainLogo[0])
+ }
+ }
+
+ b := bytes.NewBufferString("")
+ if indexTmpl, err := template.New("index").Parse(indextpl); err != nil {
+ log.Fatal("Cannot create template:", err)
+ } else if err = indexTmpl.Execute(b, tplcfg); err != nil {
+ log.Fatal("An error occurs during template execution:", err)
+ } else {
+ indexPage = b.Bytes()
+ }
}
-func StaticHandler(staticDir string) http.Handler {
- return staticRouting{staticDir}
+func serveIndex(c *gin.Context) {
+ c.Writer.Write(indexPage)
}
-func (a staticRouting) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- http.ServeFile(w, r, path.Join(a.StaticDir, "index.html"))
+var staticFS http.FileSystem
+
+func serveFile(c *gin.Context, url string) {
+ c.Request.URL.Path = url
+ http.FileServer(staticFS).ServeHTTP(c.Writer, c.Request)
+}
+
+func declareStaticRoutes(router *gin.RouterGroup, cfg *settings.Settings, baseURL string) {
+ router.GET("/", func(c *gin.Context) {
+ serveIndex(c)
+ })
+ router.GET("/auth/*_", func(c *gin.Context) {
+ serveIndex(c)
+ })
+ router.GET("/claims/*_", func(c *gin.Context) {
+ serveIndex(c)
+ })
+ router.GET("/exercices/*_", func(c *gin.Context) {
+ serveIndex(c)
+ })
+ router.GET("/events/*_", func(c *gin.Context) {
+ serveIndex(c)
+ })
+ router.GET("/files", func(c *gin.Context) {
+ serveIndex(c)
+ })
+ router.GET("/forge-links", func(c *gin.Context) {
+ serveIndex(c)
+ })
+ router.GET("/public/*_", func(c *gin.Context) {
+ serveIndex(c)
+ })
+ router.GET("/pki/*_", func(c *gin.Context) {
+ serveIndex(c)
+ })
+ router.GET("/repositories", func(c *gin.Context) {
+ serveIndex(c)
+ })
+ router.GET("/settings", func(c *gin.Context) {
+ serveIndex(c)
+ })
+ router.GET("/sync", func(c *gin.Context) {
+ serveIndex(c)
+ })
+ router.GET("/tags/*_", func(c *gin.Context) {
+ serveIndex(c)
+ })
+ router.GET("/teams/*_", func(c *gin.Context) {
+ serveIndex(c)
+ })
+ router.GET("/themes/*_", func(c *gin.Context) {
+ serveIndex(c)
+ })
+
+ router.GET("/css/*_", func(c *gin.Context) {
+ serveFile(c, strings.TrimPrefix(c.Request.URL.Path, baseURL))
+ })
+ router.GET("/fonts/*_", func(c *gin.Context) {
+ serveFile(c, strings.TrimPrefix(c.Request.URL.Path, baseURL))
+ })
+ router.GET("/img/*_", func(c *gin.Context) {
+ serveFile(c, strings.TrimPrefix(c.Request.URL.Path, baseURL))
+ })
+ router.GET("/js/*_", func(c *gin.Context) {
+ serveFile(c, strings.TrimPrefix(c.Request.URL.Path, baseURL))
+ })
+ router.GET("/views/*_", func(c *gin.Context) {
+ serveFile(c, strings.TrimPrefix(c.Request.URL.Path, baseURL))
+ })
+
+ router.GET("/files/*_", func(c *gin.Context) {
+ filepath := path.Join(fic.FilesDir, strings.TrimPrefix(strings.TrimPrefix(c.Request.URL.Path, baseURL), "/files"))
+
+ if st, err := os.Stat(filepath); os.IsNotExist(err) || st.Size() == 0 {
+ if st, err := os.Stat(filepath + ".gz"); err == nil {
+ if fd, err := os.Open(filepath + ".gz"); err == nil {
+ c.DataFromReader(http.StatusOK, st.Size(), "application/octet-stream", fd, map[string]string{
+ "Content-Encoding": "gzip",
+ })
+ return
+ }
+ }
+ }
+
+ c.File(filepath)
+ })
+ router.GET("/submissions/*_", func(c *gin.Context) {
+ http.ServeFile(c.Writer, c.Request, path.Join(api.TimestampCheck, strings.TrimPrefix(c.Request.URL.Path, path.Join(baseURL, "submissions"))))
+ })
+ router.GET("/vids/*_", func(c *gin.Context) {
+ if importer, ok := sync.GlobalImporter.(sync.DirectAccessImporter); ok {
+ http.ServeFile(c.Writer, c.Request, importer.GetLocalPath(strings.TrimPrefix(c.Request.URL.Path, path.Join(baseURL, "vids"))))
+ } else {
+ c.AbortWithError(http.StatusBadRequest, errors.New("Only available with local importer."))
+ }
+ })
+
+ router.GET("/check_import.html", func(c *gin.Context) {
+ serveFile(c, "check_import.html")
+ })
+ router.GET("/full_import_report.json", func(c *gin.Context) {
+ http.ServeFile(c.Writer, c.Request, sync.DeepReportPath)
+ })
}
diff --git a/admin/static/check_import.html b/admin/static/check_import.html
new file mode 100644
index 00000000..da2176e6
--- /dev/null
+++ b/admin/static/check_import.html
@@ -0,0 +1,52 @@
+
+
+
+ Rapport d'import FIC
+
+
+
+
+
+
+ Rapport d'import FIC
+
+ Date du dernier import :
+
+
+
+
+
diff --git a/admin/static/css/bootstrap.min.css b/admin/static/css/bootstrap.min.css
index 4cf729e4..83a71b1f 100644
--- a/admin/static/css/bootstrap.min.css
+++ b/admin/static/css/bootstrap.min.css
@@ -1,6 +1,7 @@
/*!
- * Bootstrap v3.3.6 (http://getbootstrap.com)
- * Copyright 2011-2015 Twitter, Inc.
- * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
- *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */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,menu,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-color:transparent}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;padding:0}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}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!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^="javascript:"]:after,a[href^="#"]: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}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!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.woff2) format('woff2'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#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:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-eur:before,.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"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}: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:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;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 a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;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}[role=button]{cursor:pointer}.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%}.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:#337ab7}a.text-primary:focus,a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:focus,a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,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-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;margin-left:-5px;list-style:none}.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'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;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%;font-weight:700;-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-10,.col-lg-11,.col-lg-12,.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-md-1,.col-md-10,.col-md-11,.col-md-12,.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-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.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-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{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-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{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-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{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-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{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}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}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{border:1px solid #ddd}.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-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{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}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-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;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;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=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;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 ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.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:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{background-color:transparent;border:0}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-top:4px\9;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;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}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}.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{cursor:not-allowed}.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{cursor:not-allowed}.form-control-static{min-height:34px;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}.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-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.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,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{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,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{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,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{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~.form-control-feedback{top:25px}.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 .form-control-static{display:inline-block}.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{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.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;-ms-touch-action:manipulation;touch-action:manipulation;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,.btn:active.focus,.btn:active:focus,.btn:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.focus,.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{cursor:not-allowed;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none;opacity:.65}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,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:#337ab7;border-color:#2e6da4}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#255625}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,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.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,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.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,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.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,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:#337ab7;border-radius:0}.btn-link,.btn-link.active,.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:#23527c;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.3333333;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-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{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:#337ab7;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 dashed;border-bottom:4px solid\9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@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 .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,.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:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-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;border-bottom-width: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-left-radius:4px;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-right-radius:4px;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],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.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 .form-control:focus{z-index:3}.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.3333333;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 .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn: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 .form-control:first-child,.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){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.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{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.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{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;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:#337ab7}.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 #ddd;border-bottom-color: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:#337ab7}.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 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.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-device-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}@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 15px;font-size:18px;line-height:20px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .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-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.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 .form-control-static{display:inline-block}.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}.navbar-form .form-group:last-child{margin-bottom:0}}@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-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{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;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}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{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-text{color:#777}.navbar-default .navbar-nav>li>a{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:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.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:#9d9d9d}.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:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.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:#337ab7;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{z-index:2;color:#23527c;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:3;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.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;line-height:1.3333333}.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;line-height:1.5}.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:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.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:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.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,.container-fluid .jumbotron{padding-right:15px;padding-left:15px;border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .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:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.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{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{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:#337ab7;-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,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,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-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.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}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#777;cursor:not-allowed;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:#337ab7;border-color:#337ab7}.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:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.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,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.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,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.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,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.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,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.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,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.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,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.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,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.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{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>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,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.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,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px;padding-left:15px}.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,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr: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,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr: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,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{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>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>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.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-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{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>.list-group,.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:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.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,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.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-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-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:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,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{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-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:12px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;filter:alpha(opacity=0);opacity:0;line-break:auto}.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;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{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;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;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-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;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;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);line-break:auto}.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;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{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{left:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{left:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.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);background-color:rgba(0,0,0,0);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);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(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);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) 0,rgba(0,0,0,.5) 100%);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(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);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;margin-top:-10px}.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;font-family:serif;line-height:1}.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:rgba(0,0,0,0);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:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.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,.modal-header:after,.modal-header: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,.modal-header: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}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-md,.visible-sm,.visible-xs{display:none!important}.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.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!important}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!important}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!important}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!important}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!important}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 v4.6.2 (https://getbootstrap.com/)
+ * Copyright 2011-2022 The Bootstrap Authors
+ * Copyright 2011-2022 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
+ */:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([class]){color:inherit;text-decoration:none}a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit;text-align:-webkit-match-parent}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:.875em;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code{font-size:87.5%;color:#e83e8c;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.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-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.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-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.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-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-ms-flex-order:-1;order:-1}.order-last{-ms-flex-order:13;order:13}.order-0{-ms-flex-order:0;order:0}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-sm-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-sm-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-sm-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-sm-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-sm-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-sm-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-ms-flex-order:-1;order:-1}.order-sm-last{-ms-flex-order:13;order:13}.order-sm-0{-ms-flex-order:0;order:0}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-md-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-md-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-md-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-md-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-md-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-md-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-md-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-ms-flex-order:-1;order:-1}.order-md-last{-ms-flex-order:13;order:13}.order-md-0{-ms-flex-order:0;order:0}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-lg-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-lg-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-lg-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-lg-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-lg-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-lg-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-ms-flex-order:-1;order:-1}.order-lg-last{-ms-flex-order:13;order:13}.order-lg-0{-ms-flex-order:0;order:0}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-xl-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-xl-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-xl-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-xl-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-xl-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-xl-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-ms-flex-order:-1;order:-1}.order-xl-last{-ms-flex-order:13;order:13}.order-xl-0{-ms-flex-order:0;order:0}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;margin-bottom:1rem;color:#212529}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered td,.table-bordered th{border:1px solid #dee2e6}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{color:#212529;background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#b8daff}.table-primary tbody+tbody,.table-primary td,.table-primary th,.table-primary thead th{border-color:#7abaff}.table-hover .table-primary:hover{background-color:#9fcdff}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#9fcdff}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#d6d8db}.table-secondary tbody+tbody,.table-secondary td,.table-secondary th,.table-secondary thead th{border-color:#b3b7bb}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>td,.table-success>th{background-color:#c3e6cb}.table-success tbody+tbody,.table-success td,.table-success th,.table-success thead th{border-color:#8fd19e}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>td,.table-info>th{background-color:#bee5eb}.table-info tbody+tbody,.table-info td,.table-info th,.table-info thead th{border-color:#86cfda}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abdde5}.table-warning,.table-warning>td,.table-warning>th{background-color:#ffeeba}.table-warning tbody+tbody,.table-warning td,.table-warning th,.table-warning thead th{border-color:#ffdf7e}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c6cb}.table-danger tbody+tbody,.table-danger td,.table-danger th,.table-danger thead th{border-color:#ed969e}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-light tbody+tbody,.table-light td,.table-light th,.table-light thead th{border-color:#fbfcfc}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#95999c}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#343a40;border-color:#454d55}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#343a40}.table-dark td,.table-dark th,.table-dark thead th{border-color:#454d55}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.form-control::-webkit-input-placeholder{color:#6c757d;opacity:1}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control:-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}input[type=date].form-control,input[type=datetime-local].form-control,input[type=month].form-control,input[type=time].form-control{-webkit-appearance:none;-moz-appearance:none;appearance:none}select.form-control:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;font-size:1rem;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}select.form-control[multiple],select.form-control[size]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#28a745}.valid-tooltip{position:absolute;top:100%;left:0;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(40,167,69,.9);border-radius:.25rem}.form-row>.col>.valid-tooltip,.form-row>[class*=col-]>.valid-tooltip{left:5px}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#28a745;padding-right:calc(1.5em + .75rem)!important;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.was-validated select.form-control:valid,select.form-control.is-valid{padding-right:3rem!important;background-position:right 1.5rem center}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-valid,.was-validated .custom-select:valid{border-color:#28a745;padding-right:calc(.75em + 2.3125rem)!important;background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") right .75rem center/8px 10px no-repeat,#fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem) no-repeat}.custom-select.is-valid:focus,.was-validated .custom-select:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#28a745}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#28a745}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{border-color:#28a745}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{border-color:#34ce57;background-color:#34ce57}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-control-input.is-valid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:valid:focus:not(:checked)~.custom-control-label::before{border-color:#28a745}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#28a745}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;left:0;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.form-row>.col>.invalid-tooltip,.form-row>[class*=col-]>.invalid-tooltip{left:5px}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem)!important;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.was-validated select.form-control:invalid,select.form-control.is-invalid{padding-right:3rem!important;background-position:right 1.5rem center}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-invalid,.was-validated .custom-select:invalid{border-color:#dc3545;padding-right:calc(.75em + 2.3125rem)!important;background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") right .75rem center/8px 10px no-repeat,#fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e") center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem) no-repeat}.custom-select.is-invalid:focus,.was-validated .custom-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#dc3545}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{border-color:#dc3545}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{border-color:#e4606d;background-color:#e4606d}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-control-input.is-invalid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus:not(:checked)~.custom-control-label::before{border-color:#dc3545}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#dc3545}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-inline{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;-ms-flex-negative:0;flex-shrink:0;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#212529;text-align:center;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529;text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.btn.disabled,.btn:disabled{opacity:.65}.btn:not(:disabled):not(.disabled){cursor:pointer}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:hover{color:#fff;background-color:#0069d9;border-color:#0062cc}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#0069d9;border-color:#0062cc;box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0062cc;border-color:#005cbf}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary.focus,.btn-secondary:focus{color:#fff;background-color:#5a6268;border-color:#545b62;box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#218838;border-color:#1e7e34;box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#138496;border-color:#117a8b;box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#117a8b;border-color:#10707f}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#212529;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{color:#212529;background-color:#e0a800;border-color:#d39e00;box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#212529;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c82333;border-color:#bd2130;box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#212529;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{color:#212529;background-color:#e2e6ea;border-color:#dae0e5;box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#212529;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{color:#fff;background-color:#23272b;border-color:#1d2124;box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-outline-primary{color:#007bff;border-color:#007bff}.btn-outline-primary:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#007bff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-success{color:#28a745;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-info{color:#17a2b8;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-dark{color:#343a40;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#007bff;text-decoration:none}.btn-link:hover{color:#0056b3;text-decoration:underline}.btn-link.focus,.btn-link:focus{text-decoration:underline}.btn-link.disabled,.btn-link:disabled{color:#6c757d;pointer-events:none}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.width{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.width{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#e9ecef}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#007bff}.dropdown-item.disabled,.dropdown-item:disabled{color:#adb5bd;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#212529}.btn-group,.btn-group-vertical{position:relative;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-ms-flex:1 1 auto;flex:1 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-toolbar{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{-ms-flex-direction:column;flex-direction:column;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:center;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control,.input-group>.form-control-plaintext{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;min-width:0;margin-bottom:0}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control,.input-group>.form-control-plaintext+.custom-file,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.form-control{margin-left:-1px}.input-group>.custom-file .custom-file-input:focus~.custom-file-label,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group:not(.has-validation)>.custom-file:not(:last-child) .custom-file-label,.input-group:not(.has-validation)>.custom-file:not(:last-child) .custom-file-label::after,.input-group:not(.has-validation)>.custom-select:not(:last-child),.input-group:not(.has-validation)>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.custom-file:nth-last-child(n+3) .custom-file-label,.input-group.has-validation>.custom-file:nth-last-child(n+3) .custom-file-label::after,.input-group.has-validation>.custom-select:nth-last-child(n+3),.input-group.has-validation>.form-control:nth-last-child(n+3){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-append,.input-group-prepend{display:-ms-flexbox;display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn:focus,.input-group-prepend .btn:focus{z-index:3}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group-lg>.custom-select,.input-group-lg>.form-control:not(textarea){height:calc(1.5em + 1rem + 2px)}.input-group-lg>.custom-select,.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.custom-select,.input-group-sm>.form-control:not(textarea){height:calc(1.5em + .5rem + 2px)}.input-group-sm>.custom-select,.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group.has-validation>.input-group-append:nth-last-child(n+3)>.btn,.input-group.has-validation>.input-group-append:nth-last-child(n+3)>.input-group-text,.input-group:not(.has-validation)>.input-group-append:not(:last-child)>.btn,.input-group:not(.has-validation)>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;z-index:1;display:block;min-height:1.5rem;padding-left:1.5rem;-webkit-print-color-adjust:exact;color-adjust:exact;print-color-adjust:exact}.custom-control-inline{display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;left:0;z-index:-1;width:1rem;height:1.25rem;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;border-color:#007bff;background-color:#007bff}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-control-input:focus:not(:checked)~.custom-control-label::before{border-color:#80bdff}.custom-control-input:not(:disabled):active~.custom-control-label::before{color:#fff;background-color:#b3d7ff;border-color:#b3d7ff}.custom-control-input:disabled~.custom-control-label,.custom-control-input[disabled]~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label::before,.custom-control-input[disabled]~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#fff;border:1px solid #adb5bd}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:50%/50% 50% no-repeat}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{border-color:#007bff;background-color:#007bff}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:.5rem}.custom-switch .custom-control-label::after{top:calc(.25rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:.5rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-switch .custom-control-label::after{transition:none}}.custom-switch .custom-control-input:checked~.custom-control-label::after{background-color:#fff;-webkit-transform:translateX(.75rem);transform:translateX(.75rem)}.custom-switch .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem 1.75rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;vertical-align:middle;background:#fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") right .75rem center/8px 10px no-repeat;border:1px solid #ced4da;border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{display:none}.custom-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.custom-select-sm{height:calc(1.5em + .5rem + 2px);padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(1.5em + .75rem + 2px);margin:0;overflow:hidden;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#80bdff;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-file-input:disabled~.custom-file-label,.custom-file-input[disabled]~.custom-file-label{background-color:#e9ecef}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-input~.custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;overflow:hidden;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(1.5em + .75rem);padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:inherit;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;height:1.4rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#007bff;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#b3d7ff}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#007bff;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-moz-range-thumb{-moz-transition:none;transition:none}}.custom-range::-moz-range-thumb:active{background-color:#b3d7ff}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#007bff;border:0;border-radius:1rem;-ms-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-ms-thumb{-ms-transition:none;transition:none}}.custom-range::-ms-thumb:active{background-color:#b3d7ff}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link{margin-bottom:-1px;background-color:transparent;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{isolation:isolate;border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{background:0 0;border:0;border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#007bff}.nav-fill .nav-item,.nav-fill>.nav-link{-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:.5rem 1rem}.navbar .container,.navbar .container-fluid,.navbar .container-lg,.navbar .container-md,.navbar .container-sm,.navbar .container-xl{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-ms-flex-positive:1;flex-grow:1;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:50%/100% 100% no-repeat}.navbar-nav-scroll{max-height:75vh;overflow-y:auto}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a{color:rgba(0,0,0,.9)}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.5);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{-ms-flex:1 1 auto;flex:1 1 auto;min-height:1px;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{-ms-flex-negative:0;flex-shrink:0;width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck .card{margin-bottom:15px}@media (min-width:576px){.card-deck{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{-ms-flex:1 0 0%;flex:1 0 0%;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group>.card{margin-bottom:15px}@media (min-width:576px){.card-group{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion{overflow-anchor:none}.accordion>.card{overflow:hidden}.accordion>.card:not(:last-of-type){border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:not(:first-of-type){border-top-left-radius:0;border-top-right-radius:0}.accordion>.card>.card-header{border-radius:0;margin-bottom:-1px}.breadcrumb{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#007bff;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{z-index:2;color:#0056b3;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:3;color:#fff;background-color:#007bff;border-color:#007bff}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.badge{transition:none}}a.badge:focus,a.badge:hover{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#007bff}a.badge-primary:focus,a.badge-primary:hover{color:#fff;background-color:#0062cc}a.badge-primary.focus,a.badge-primary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.badge-secondary{color:#fff;background-color:#6c757d}a.badge-secondary:focus,a.badge-secondary:hover{color:#fff;background-color:#545b62}a.badge-secondary.focus,a.badge-secondary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.badge-success{color:#fff;background-color:#28a745}a.badge-success:focus,a.badge-success:hover{color:#fff;background-color:#1e7e34}a.badge-success.focus,a.badge-success:focus{outline:0;box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.badge-info{color:#fff;background-color:#17a2b8}a.badge-info:focus,a.badge-info:hover{color:#fff;background-color:#117a8b}a.badge-info.focus,a.badge-info:focus{outline:0;box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.badge-warning{color:#212529;background-color:#ffc107}a.badge-warning:focus,a.badge-warning:hover{color:#212529;background-color:#d39e00}a.badge-warning.focus,a.badge-warning:focus{outline:0;box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.badge-danger{color:#fff;background-color:#dc3545}a.badge-danger:focus,a.badge-danger:hover{color:#fff;background-color:#bd2130}a.badge-danger.focus,a.badge-danger:focus{outline:0;box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.badge-light{color:#212529;background-color:#f8f9fa}a.badge-light:focus,a.badge-light:hover{color:#212529;background-color:#dae0e5}a.badge-light.focus,a.badge-light:focus{outline:0;box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.badge-dark{color:#fff;background-color:#343a40}a.badge-dark:focus,a.badge-dark:hover{color:#fff;background-color:#1d2124}a.badge-dark.focus,a.badge-dark:focus{outline:0;box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;z-index:2;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-primary hr{border-top-color:#9fcdff}.alert-primary .alert-link{color:#002752}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-info hr{border-top-color:#abdde5}.alert-info .alert-link{color:#062c33}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;line-height:0;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#007bff;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.media{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start}.media-body{-ms-flex:1;flex:1}.list-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#007bff;border-color:#007bff}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#004085;background-color:#b8daff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#004085;background-color:#9fcdff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#004085;border-color:#004085}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#0c5460;background-color:#bee5eb}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#0c5460;background-color:#abdde5}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#0c5460;border-color:#0c5460}.list-group-item-warning{color:#856404;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#856404;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:hover{color:#000;text-decoration:none}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover{opacity:.75}button.close{padding:0;background-color:transparent;border:0}a.close.disabled{pointer-events:none}.toast{-ms-flex-preferred-size:350px;flex-basis:350px;max-width:350px;font-size:.875rem;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .25rem .75rem rgba(0,0,0,.1);opacity:0;border-radius:.25rem}.toast:not(:last-child){margin-bottom:.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.25rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-body{padding:.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out;-webkit-transform:translate(0,-50px);transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{-webkit-transform:none;transform:none}.modal.modal-static .modal-dialog{-webkit-transform:scale(1.02);transform:scale(1.02)}.modal-dialog-scrollable{display:-ms-flexbox;display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-footer,.modal-dialog-scrollable .modal-header{-ms-flex-negative:0;flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);height:-webkit-min-content;height:-moz-min-content;height:min-content;content:""}.modal-dialog-centered.modal-dialog-scrollable{-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .close{padding:1rem 1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:end;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem);height:-webkit-min-content;height:-moz-min-content;height:min-content}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top]>.arrow,.bs-popover-top>.arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=top]>.arrow::before,.bs-popover-top>.arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top]>.arrow::after,.bs-popover-top>.arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right]>.arrow,.bs-popover-right>.arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right]>.arrow::before,.bs-popover-right>.arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right]>.arrow::after,.bs-popover-right>.arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom]>.arrow,.bs-popover-bottom>.arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=bottom]>.arrow::before,.bs-popover-bottom>.arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom]>.arrow::after,.bs-popover-bottom>.arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left]>.arrow,.bs-popover-left>.arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left]>.arrow::before,.bs-popover-left>.arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left]>.arrow::after,.bs-popover-left>.arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{-ms-touch-action:pan-y;touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-right,.carousel-item-next:not(.carousel-item-left){-webkit-transform:translateX(100%);transform:translateX(100%)}.active.carousel-item-left,.carousel-item-prev:not(.carousel-item-right){-webkit-transform:translateX(-100%);transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;-webkit-transform:none;transform:none}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:50%/100% 100% no-repeat}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{box-sizing:content-box;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators li{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@-webkit-keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25em solid currentcolor;border-right-color:transparent;border-radius:50%;-webkit-animation:.75s linear infinite spinner-border;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1;-webkit-transform:none;transform:none}}@keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1;-webkit-transform:none;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;background-color:currentcolor;border-radius:50%;opacity:0;-webkit-animation:.75s linear infinite spinner-grow;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#007bff!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#0062cc!important}.bg-secondary{background-color:#6c757d!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62!important}.bg-success{background-color:#28a745!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#1e7e34!important}.bg-info{background-color:#17a2b8!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#117a8b!important}.bg-warning{background-color:#ffc107!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00!important}.bg-danger{background-color:#dc3545!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#bd2130!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dee2e6!important}.border-top{border-top:1px solid #dee2e6!important}.border-right{border-right:1px solid #dee2e6!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-left{border-left:1px solid #dee2e6!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#007bff!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#28a745!important}.border-info{border-color:#17a2b8!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded-sm{border-radius:.2rem!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-right{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-lg{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-sm-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-md-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-lg-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-xl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;-ms-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;-ms-user-select:none!important;user-select:none!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position:-webkit-sticky) or (position:sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.min-vw-100{min-width:100vw!important}.min-vh-100{min-height:100vh!important}.vw-100{width:100vw!important}.vh-100{height:100vh!important}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:rgba(0,0,0,0)}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace!important}.text-justify{text-align:justify!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-lighter{font-weight:lighter!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-weight-bolder{font-weight:bolder!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#007bff!important}a.text-primary:focus,a.text-primary:hover{color:#0056b3!important}.text-secondary{color:#6c757d!important}a.text-secondary:focus,a.text-secondary:hover{color:#494f54!important}.text-success{color:#28a745!important}a.text-success:focus,a.text-success:hover{color:#19692c!important}.text-info{color:#17a2b8!important}a.text-info:focus,a.text-info:hover{color:#0f6674!important}.text-warning{color:#ffc107!important}a.text-warning:focus,a.text-warning:hover{color:#ba8b00!important}.text-danger{color:#dc3545!important}a.text-danger:focus,a.text-danger:hover{color:#a71d2a!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#cbd3da!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#121416!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none!important}.text-break{word-break:break-word!important;word-wrap:break-word!important}.text-reset{color:inherit!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #adb5bd;page-break-inside:avoid}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #dee2e6!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#dee2e6}.table .thead-dark th{color:inherit;border-color:#dee2e6}}
/*# sourceMappingURL=bootstrap.min.css.map */
\ No newline at end of file
diff --git a/admin/static/css/glyphicon.css b/admin/static/css/glyphicon.css
new file mode 100644
index 00000000..1bf9e4de
--- /dev/null
+++ b/admin/static/css/glyphicon.css
@@ -0,0 +1,805 @@
+@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.woff2') format('woff2'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');
+}
+.glyphicon {
+ position: relative;
+ top: 1px;
+ display: inline-block;
+ font-family: 'Glyphicons Halflings';
+ font-style: normal;
+ font-weight: normal;
+ line-height: 1;
+
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+.glyphicon-asterisk:before {
+ content: "\002a";
+}
+.glyphicon-plus:before {
+ content: "\002b";
+}
+.glyphicon-euro:before,
+.glyphicon-eur: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";
+}
+.glyphicon-cd:before {
+ content: "\e201";
+}
+.glyphicon-save-file:before {
+ content: "\e202";
+}
+.glyphicon-open-file:before {
+ content: "\e203";
+}
+.glyphicon-level-up:before {
+ content: "\e204";
+}
+.glyphicon-copy:before {
+ content: "\e205";
+}
+.glyphicon-paste:before {
+ content: "\e206";
+}
+.glyphicon-alert:before {
+ content: "\e209";
+}
+.glyphicon-equalizer:before {
+ content: "\e210";
+}
+.glyphicon-king:before {
+ content: "\e211";
+}
+.glyphicon-queen:before {
+ content: "\e212";
+}
+.glyphicon-pawn:before {
+ content: "\e213";
+}
+.glyphicon-bishop:before {
+ content: "\e214";
+}
+.glyphicon-knight:before {
+ content: "\e215";
+}
+.glyphicon-baby-formula:before {
+ content: "\e216";
+}
+.glyphicon-tent:before {
+ content: "\26fa";
+}
+.glyphicon-blackboard:before {
+ content: "\e218";
+}
+.glyphicon-bed:before {
+ content: "\e219";
+}
+.glyphicon-apple:before {
+ content: "\f8ff";
+}
+.glyphicon-erase:before {
+ content: "\e221";
+}
+.glyphicon-hourglass:before {
+ content: "\231b";
+}
+.glyphicon-lamp:before {
+ content: "\e223";
+}
+.glyphicon-duplicate:before {
+ content: "\e224";
+}
+.glyphicon-piggy-bank:before {
+ content: "\e225";
+}
+.glyphicon-scissors:before {
+ content: "\e226";
+}
+.glyphicon-bitcoin:before {
+ content: "\e227";
+}
+.glyphicon-btc:before {
+ content: "\e227";
+}
+.glyphicon-xbt:before {
+ content: "\e227";
+}
+.glyphicon-yen:before {
+ content: "\00a5";
+}
+.glyphicon-jpy:before {
+ content: "\00a5";
+}
+.glyphicon-ruble:before {
+ content: "\20bd";
+}
+.glyphicon-rub:before {
+ content: "\20bd";
+}
+.glyphicon-scale:before {
+ content: "\e230";
+}
+.glyphicon-ice-lolly:before {
+ content: "\e231";
+}
+.glyphicon-ice-lolly-tasted:before {
+ content: "\e232";
+}
+.glyphicon-education:before {
+ content: "\e233";
+}
+.glyphicon-option-horizontal:before {
+ content: "\e234";
+}
+.glyphicon-option-vertical:before {
+ content: "\e235";
+}
+.glyphicon-menu-hamburger:before {
+ content: "\e236";
+}
+.glyphicon-modal-window:before {
+ content: "\e237";
+}
+.glyphicon-oil:before {
+ content: "\e238";
+}
+.glyphicon-grain:before {
+ content: "\e239";
+}
+.glyphicon-sunglasses:before {
+ content: "\e240";
+}
+.glyphicon-text-size:before {
+ content: "\e241";
+}
+.glyphicon-text-color:before {
+ content: "\e242";
+}
+.glyphicon-text-background:before {
+ content: "\e243";
+}
+.glyphicon-object-align-top:before {
+ content: "\e244";
+}
+.glyphicon-object-align-bottom:before {
+ content: "\e245";
+}
+.glyphicon-object-align-horizontal:before {
+ content: "\e246";
+}
+.glyphicon-object-align-left:before {
+ content: "\e247";
+}
+.glyphicon-object-align-vertical:before {
+ content: "\e248";
+}
+.glyphicon-object-align-right:before {
+ content: "\e249";
+}
+.glyphicon-triangle-right:before {
+ content: "\e250";
+}
+.glyphicon-triangle-left:before {
+ content: "\e251";
+}
+.glyphicon-triangle-bottom:before {
+ content: "\e252";
+}
+.glyphicon-triangle-top:before {
+ content: "\e253";
+}
+.glyphicon-console:before {
+ content: "\e254";
+}
+.glyphicon-superscript:before {
+ content: "\e255";
+}
+.glyphicon-subscript:before {
+ content: "\e256";
+}
+.glyphicon-menu-left:before {
+ content: "\e257";
+}
+.glyphicon-menu-right:before {
+ content: "\e258";
+}
+.glyphicon-menu-down:before {
+ content: "\e259";
+}
+.glyphicon-menu-up:before {
+ content: "\e260";
+}
diff --git a/admin/static/css/slate.min.css b/admin/static/css/slate.min.css
deleted file mode 100644
index af7e2033..00000000
--- a/admin/static/css/slate.min.css
+++ /dev/null
@@ -1,11 +0,0 @@
-/*!
- * bootswatch v3.3.6
- * Homepage: http://bootswatch.com
- * Copyright 2012-2015 Thomas Park
- * Licensed under MIT
- * Based on Bootstrap
-*//*!
- * Bootstrap v3.3.6 (http://getbootstrap.com)
- * Copyright 2011-2015 Twitter, Inc.
- * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
- *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,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-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:0.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace, monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}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{border:0;padding: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;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,*:before,*:after{background:transparent !important;color:#000 !important;-webkit-box-shadow:none !important;box-shadow:none !important;text-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:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000 !important}.label{border:1px solid #000}.table{border-collapse:collapse !important}.table td,.table th{background-color:#fff !important}.table-bordered th,.table-bordered td{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.woff2') format('woff2'),url('../fonts/glyphicons-halflings-regular.woff') format('woff'),url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'),url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-euro:before,.glyphicon-eur: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"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}*:before,*:after{-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:#c8c8c8;background-color:#272b30}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#ffffff;text-decoration:none}a:hover,a:focus{color:#ffffff;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}.img-responsive,.thumbnail>img,.thumbnail a>img,.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#1c1e22;border:1px solid #0c0d0e;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #1c1e22}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;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}[role="button"]{cursor:pointer}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:normal;line-height:1;color:#7a8288}h1,.h1,h2,.h2,h3,.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,.h4,h5,.h5,h6,.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%}mark,.mark{background-color:#f89406;padding:.2em}.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:#7a8288}.text-primary{color:#7a8288}a.text-primary:hover,a.text-primary:focus{color:#62686d}.text-success{color:#ffffff}a.text-success:hover,a.text-success:focus{color:#e6e6e6}.text-info{color:#ffffff}a.text-info:hover,a.text-info:focus{color:#e6e6e6}.text-warning{color:#ffffff}a.text-warning:hover,a.text-warning:focus{color:#e6e6e6}.text-danger{color:#ffffff}a.text-danger:hover,a.text-danger:focus{color:#e6e6e6}.bg-primary{color:#fff;background-color:#7a8288}a.bg-primary:hover,a.bg-primary:focus{background-color:#62686d}.bg-success{background-color:#62c462}a.bg-success:hover,a.bg-success:focus{background-color:#42b142}.bg-info{background-color:#5bc0de}a.bg-info:hover,a.bg-info:focus{background-color:#31b0d5}.bg-warning{background-color:#f89406}a.bg-warning:hover,a.bg-warning:focus{background-color:#c67605}.bg-danger{background-color:#ee5f5b}a.bg-danger:hover,a.bg-danger:focus{background-color:#e9322d}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #1c1e22}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-left:5px;padding-right:5px}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.42857143}dt{font-weight:bold}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #7a8288}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #7a8288}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.42857143;color:#7a8288}blockquote footer:before,blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #7a8288;border-left:0;text-align:right}.blockquote-reverse footer:before,blockquote.pull-right footer:before,.blockquote-reverse small:before,blockquote.pull-right small:before,.blockquote-reverse .small:before,blockquote.pull-right .small:before{content:''}.blockquote-reverse footer:after,blockquote.pull-right footer:after,.blockquote-reverse small:after,blockquote.pull-right small:after,.blockquote-reverse .small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#ffffff;background-color:#333333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.25)}kbd kbd{padding:0;font-size:100%;font-weight:bold;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;word-break:break-all;word-wrap:break-word;color:#3a3f44;background-color:#f5f5f5;border:1px solid #cccccc;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{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.row{margin-left:-15px;margin-right:-15px}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-left:15px;padding-right: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:#2e3338}caption{padding-top:8px;padding-bottom:8px;color:#7a8288;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #1c1e22}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #1c1e22}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #1c1e22}.table .table{background-color:#272b30}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #1c1e22}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #1c1e22}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#353a41}.table-hover>tbody>tr:hover{background-color:#49515a}table col[class*="col-"]{position:static;float:none;display:table-column}table td[class*="col-"],table th[class*="col-"]{position:static;float:none;display:table-cell}.table>thead>tr>td.active,.table>tbody>tr>td.active,.table>tfoot>tr>td.active,.table>thead>tr>th.active,.table>tbody>tr>th.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>tbody>tr.active>td,.table>tfoot>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr.active>th,.table>tfoot>tr.active>th{background-color:#49515a}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr.active:hover>th{background-color:#3e444c}.table>thead>tr>td.success,.table>tbody>tr>td.success,.table>tfoot>tr>td.success,.table>thead>tr>th.success,.table>tbody>tr>th.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>tbody>tr.success>td,.table>tfoot>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr.success>th,.table>tfoot>tr.success>th{background-color:#62c462}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr.success:hover>th{background-color:#4fbd4f}.table>thead>tr>td.info,.table>tbody>tr>td.info,.table>tfoot>tr>td.info,.table>thead>tr>th.info,.table>tbody>tr>th.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>tbody>tr.info>td,.table>tfoot>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr.info>th,.table>tfoot>tr.info>th{background-color:#5bc0de}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr.info:hover>th{background-color:#46b8da}.table>thead>tr>td.warning,.table>tbody>tr>td.warning,.table>tfoot>tr>td.warning,.table>thead>tr>th.warning,.table>tbody>tr>th.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>tbody>tr.warning>td,.table>tfoot>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr.warning>th,.table>tfoot>tr.warning>th{background-color:#f89406}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr.warning:hover>th{background-color:#df8505}.table>thead>tr>td.danger,.table>tbody>tr>td.danger,.table>tfoot>tr>td.danger,.table>thead>tr>th.danger,.table>tbody>tr>th.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>tbody>tr.danger>td,.table>tfoot>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr.danger>th,.table>tfoot>tr.danger>th{background-color:#ee5f5b}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr.danger:hover>th{background-color:#ec4844}.table-responsive{overflow-x:auto;min-height:0.01%}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #1c1e22}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{padding:0;margin:0;border:0;min-width:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#c8c8c8;border:0;border-bottom:1px solid #1c1e22}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:bold}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type="radio"],input[type="checkbox"]{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="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:9px;font-size:14px;line-height:1.42857143;color:#272b30}.form-control{display:block;width:100%;height:38px;padding:8px 12px;font-size:14px;line-height:1.42857143;color:#272b30;background-color:#ffffff;background-image:none;border:1px solid #cccccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(102,175,233,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(102,175,233,0.6)}.form-control::-moz-placeholder{color:#7a8288;opacity:1}.form-control:-ms-input-placeholder{color:#7a8288}.form-control::-webkit-input-placeholder{color:#7a8288}.form-control::-ms-expand{border:0;background-color:transparent}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#999999;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}input[type="search"]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type="date"].form-control,input[type="time"].form-control,input[type="datetime-local"].form-control,input[type="month"].form-control{line-height:38px}input[type="date"].input-sm,input[type="time"].input-sm,input[type="datetime-local"].input-sm,input[type="month"].input-sm,.input-group-sm input[type="date"],.input-group-sm input[type="time"],.input-group-sm input[type="datetime-local"],.input-group-sm input[type="month"]{line-height:30px}input[type="date"].input-lg,input[type="time"].input-lg,input[type="datetime-local"].input-lg,input[type="month"].input-lg,.input-group-lg input[type="date"],.input-group-lg input[type="time"],.input-group-lg input[type="datetime-local"],.input-group-lg input[type="month"]{line-height:54px}}.form-group{margin-bottom:15px}.radio,.checkbox{position:relative;display:block;margin-top:10px;margin-bottom:10px}.radio label,.checkbox label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:normal;cursor:pointer}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{position:absolute;margin-left:-20px;margin-top:4px \9}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:normal;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"].disabled,input[type="checkbox"].disabled,fieldset[disabled] input[type="radio"],fieldset[disabled] input[type="checkbox"]{cursor:not-allowed}.radio-inline.disabled,.checkbox-inline.disabled,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.radio.disabled label,.checkbox.disabled label,fieldset[disabled] .radio label,fieldset[disabled] .checkbox label{cursor:not-allowed}.form-control-static{padding-top:9px;padding-bottom:9px;margin-bottom:0;min-height:34px}.form-control-static.input-lg,.form-control-static.input-sm{padding-left:0;padding-right:0}.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}textarea.input-sm,select[multiple].input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm textarea.form-control,.form-group-sm select[multiple].form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:54px;padding:14px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:54px;line-height:54px}textarea.input-lg,select[multiple].input-lg{height:auto}.form-group-lg .form-control{height:54px;padding:14px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:54px;line-height:54px}.form-group-lg textarea.form-control,.form-group-lg select[multiple].form-control{height:auto}.form-group-lg .form-control-static{height:54px;min-height:38px;padding:15px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:47.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:38px;height:38px;line-height:38px;text-align:center;pointer-events:none}.input-lg+.form-control-feedback,.input-group-lg+.form-control-feedback,.form-group-lg .form-control+.form-control-feedback{width:54px;height:54px;line-height:54px}.input-sm+.form-control-feedback,.input-group-sm+.form-control-feedback,.form-group-sm .form-control+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline,.has-success.radio label,.has-success.checkbox label,.has-success.radio-inline label,.has-success.checkbox-inline label{color:#ffffff}.has-success .form-control{border-color:#ffffff;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-success .form-control:focus{border-color:#e6e6e6;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff}.has-success .input-group-addon{color:#ffffff;border-color:#ffffff;background-color:#62c462}.has-success .form-control-feedback{color:#ffffff}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline,.has-warning.radio label,.has-warning.checkbox label,.has-warning.radio-inline label,.has-warning.checkbox-inline label{color:#ffffff}.has-warning .form-control{border-color:#ffffff;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-warning .form-control:focus{border-color:#e6e6e6;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff}.has-warning .input-group-addon{color:#ffffff;border-color:#ffffff;background-color:#f89406}.has-warning .form-control-feedback{color:#ffffff}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline,.has-error.radio label,.has-error.checkbox label,.has-error.radio-inline label,.has-error.checkbox-inline label{color:#ffffff}.has-error .form-control{border-color:#ffffff;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-error .form-control:focus{border-color:#e6e6e6;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff}.has-error .input-group-addon{color:#ffffff;border-color:#ffffff;background-color:#ee5f5b}.has-error .form-control-feedback{color:#ffffff}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#ffffff}@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 .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn,.form-inline .input-group .form-control{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .radio label,.form-inline .checkbox label{padding-left:0}.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{margin-top:0;margin-bottom:0;padding-top:9px}.form-horizontal .radio,.form-horizontal .checkbox{min-height:29px}.form-horizontal .form-group{margin-left:-15px;margin-right:-15px}@media (min-width:768px){.form-horizontal .control-label{text-align:right;margin-bottom:0;padding-top:9px}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:15px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;margin-bottom:0;font-weight:normal;text-align:center;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;padding:8px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn:focus,.btn:active:focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn.active.focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus,.btn.focus{color:#ffffff;text-decoration:none}.btn:active,.btn.active{outline:0;background-image:none;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;opacity:0.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#ffffff;background-color:#3a3f44;border-color:#3a3f44}.btn-default:focus,.btn-default.focus{color:#ffffff;background-color:#232628;border-color:#000000}.btn-default:hover{color:#ffffff;background-color:#232628;border-color:#1e2023}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{color:#ffffff;background-color:#232628;border-color:#1e2023}.btn-default:active:hover,.btn-default.active:hover,.open>.dropdown-toggle.btn-default:hover,.btn-default:active:focus,.btn-default.active:focus,.open>.dropdown-toggle.btn-default:focus,.btn-default:active.focus,.btn-default.active.focus,.open>.dropdown-toggle.btn-default.focus{color:#ffffff;background-color:#121415;border-color:#000000}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled.focus,.btn-default[disabled].focus,fieldset[disabled] .btn-default.focus{background-color:#3a3f44;border-color:#3a3f44}.btn-default .badge{color:#3a3f44;background-color:#ffffff}.btn-primary{color:#ffffff;background-color:#7a8288;border-color:#7a8288}.btn-primary:focus,.btn-primary.focus{color:#ffffff;background-color:#62686d;border-color:#3e4245}.btn-primary:hover{color:#ffffff;background-color:#62686d;border-color:#5d6368}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#ffffff;background-color:#62686d;border-color:#5d6368}.btn-primary:active:hover,.btn-primary.active:hover,.open>.dropdown-toggle.btn-primary:hover,.btn-primary:active:focus,.btn-primary.active:focus,.open>.dropdown-toggle.btn-primary:focus,.btn-primary:active.focus,.btn-primary.active.focus,.open>.dropdown-toggle.btn-primary.focus{color:#ffffff;background-color:#51565a;border-color:#3e4245}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled.focus,.btn-primary[disabled].focus,fieldset[disabled] .btn-primary.focus{background-color:#7a8288;border-color:#7a8288}.btn-primary .badge{color:#7a8288;background-color:#ffffff}.btn-success{color:#ffffff;background-color:#62c462;border-color:#62c462}.btn-success:focus,.btn-success.focus{color:#ffffff;background-color:#42b142;border-color:#2d792d}.btn-success:hover{color:#ffffff;background-color:#42b142;border-color:#40a940}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{color:#ffffff;background-color:#42b142;border-color:#40a940}.btn-success:active:hover,.btn-success.active:hover,.open>.dropdown-toggle.btn-success:hover,.btn-success:active:focus,.btn-success.active:focus,.open>.dropdown-toggle.btn-success:focus,.btn-success:active.focus,.btn-success.active.focus,.open>.dropdown-toggle.btn-success.focus{color:#ffffff;background-color:#399739;border-color:#2d792d}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled.focus,.btn-success[disabled].focus,fieldset[disabled] .btn-success.focus{background-color:#62c462;border-color:#62c462}.btn-success .badge{color:#62c462;background-color:#ffffff}.btn-info{color:#ffffff;background-color:#5bc0de;border-color:#5bc0de}.btn-info:focus,.btn-info.focus{color:#ffffff;background-color:#31b0d5;border-color:#1f7e9a}.btn-info:hover{color:#ffffff;background-color:#31b0d5;border-color:#2aabd2}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{color:#ffffff;background-color:#31b0d5;border-color:#2aabd2}.btn-info:active:hover,.btn-info.active:hover,.open>.dropdown-toggle.btn-info:hover,.btn-info:active:focus,.btn-info.active:focus,.open>.dropdown-toggle.btn-info:focus,.btn-info:active.focus,.btn-info.active.focus,.open>.dropdown-toggle.btn-info.focus{color:#ffffff;background-color:#269abc;border-color:#1f7e9a}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled.focus,.btn-info[disabled].focus,fieldset[disabled] .btn-info.focus{background-color:#5bc0de;border-color:#5bc0de}.btn-info .badge{color:#5bc0de;background-color:#ffffff}.btn-warning{color:#ffffff;background-color:#f89406;border-color:#f89406}.btn-warning:focus,.btn-warning.focus{color:#ffffff;background-color:#c67605;border-color:#7c4a03}.btn-warning:hover{color:#ffffff;background-color:#c67605;border-color:#bc7005}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{color:#ffffff;background-color:#c67605;border-color:#bc7005}.btn-warning:active:hover,.btn-warning.active:hover,.open>.dropdown-toggle.btn-warning:hover,.btn-warning:active:focus,.btn-warning.active:focus,.open>.dropdown-toggle.btn-warning:focus,.btn-warning:active.focus,.btn-warning.active.focus,.open>.dropdown-toggle.btn-warning.focus{color:#ffffff;background-color:#a36104;border-color:#7c4a03}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled.focus,.btn-warning[disabled].focus,fieldset[disabled] .btn-warning.focus{background-color:#f89406;border-color:#f89406}.btn-warning .badge{color:#f89406;background-color:#ffffff}.btn-danger{color:#ffffff;background-color:#ee5f5b;border-color:#ee5f5b}.btn-danger:focus,.btn-danger.focus{color:#ffffff;background-color:#e9322d;border-color:#b71713}.btn-danger:hover{color:#ffffff;background-color:#e9322d;border-color:#e82924}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{color:#ffffff;background-color:#e9322d;border-color:#e82924}.btn-danger:active:hover,.btn-danger.active:hover,.open>.dropdown-toggle.btn-danger:hover,.btn-danger:active:focus,.btn-danger.active:focus,.open>.dropdown-toggle.btn-danger:focus,.btn-danger:active.focus,.btn-danger.active.focus,.open>.dropdown-toggle.btn-danger.focus{color:#ffffff;background-color:#dc1c17;border-color:#b71713}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled.focus,.btn-danger[disabled].focus,fieldset[disabled] .btn-danger.focus{background-color:#ee5f5b;border-color:#ee5f5b}.btn-danger .badge{color:#ee5f5b;background-color:#ffffff}.btn-link{color:#ffffff;font-weight:normal;border-radius:0}.btn-link,.btn-link:active,.btn-link.active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#ffffff;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#7a8288;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:14px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-sm,.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs,.btn-group-xs>.btn{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="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity 0.15s linear;-o-transition:opacity 0.15s linear;transition:opacity 0.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-property:height, visibility;-o-transition-property:height, visibility;transition-property:height, visibility;-webkit-transition-duration:0.35s;-o-transition-duration:0.35s;transition-duration:0.35s;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid \9;border-right:4px solid transparent;border-left:4px solid transparent}.dropup,.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;list-style:none;font-size:14px;text-align:left;background-color:#3a3f44;border:1px solid #272b30;border:1px solid rgba(0,0,0,0.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,0.175);box-shadow:0 6px 12px rgba(0,0,0,0.175);-webkit-background-clip:padding-box;background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#272b30}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:1.42857143;color:#c8c8c8;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{text-decoration:none;color:#ffffff;background-color:#272b30}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#ffffff;text-decoration:none;outline:0;background-color:#272b30}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#7a8288}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);cursor:not-allowed}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{left:auto;right:0}.dropdown-menu-left{left:0;right:auto}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#7a8288;white-space:nowrap}.dropdown-backdrop{position:fixed;left:0;right:0;bottom:0;top:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px dashed;border-bottom:4px solid \9;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{left:auto;right:0}.navbar-right .dropdown-menu-left{left:0;right:auto}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.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,.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-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-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:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-left:12px;padding-right:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,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;border-bottom-width: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-top-left-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-right-radius:0;border-top-left-radius:0;border-bottom-right-radius:4px;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-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{float:none;display:table-cell;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="radio"],[data-toggle="buttons"]>.btn-group>.btn input[type="radio"],[data-toggle="buttons"]>.btn input[type="checkbox"],[data-toggle="buttons"]>.btn-group>.btn input[type="checkbox"]{position:absolute;clip:rect(0, 0, 0, 0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*="col-"]{float:none;padding-left:0;padding-right:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:54px;padding:14px 16px;font-size:18px;line-height:1.3333333;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:54px;line-height:54px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn,select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].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}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn,select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].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:8px 12px;font-size:14px;font-weight:normal;line-height:1;color:#272b30;text-align:center;background-color:#999999;border:1px solid #cccccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:14px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type="radio"],.input-group-addon input[type="checkbox"]{margin-top:0}.input-group .form-control:first-child,.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:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{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{z-index:2;margin-left:-1px}.nav{margin-bottom:0;padding-left:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#3e444c}.nav>li.disabled>a{color:#7a8288}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#7a8288;text-decoration:none;background-color:transparent;cursor:not-allowed}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#3e444c;border-color:#ffffff}.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 #1c1e22}.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:#1c1e22 #1c1e22 #1c1e22}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#ffffff;background-color:#3e444c;border:1px solid #1c1e22;border-bottom-color:transparent;cursor:default}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{text-align:center;margin-bottom:5px}.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:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #1c1e22}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #1c1e22;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#272b30}}.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:hover,.nav-pills>li.active>a:focus{color:#ffffff;background-color:transparent}.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{text-align:center;margin-bottom:5px}.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:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #1c1e22}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #1c1e22;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#272b30}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-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{overflow-x:visible;padding-right:15px;padding-left:15px;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);-webkit-overflow-scrolling:touch}.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-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-left:0;padding-right:0}}.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:200px}}.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{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-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{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;padding:15px 15px;font-size:18px;line-height:20px;height:50px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;margin-right:15px;padding:9px 10px;margin-top:8px;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>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{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-form{margin-left:-15px;margin-right:-15px;padding:10px 15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);margin-top:6px;margin-bottom:6px}@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 .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn,.navbar-form .input-group .form-control{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .radio label,.navbar-form .checkbox label{padding-left:0}.navbar-form .radio input[type="radio"],.navbar-form .checkbox input[type="checkbox"]{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}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;border:0;margin-left:0;margin-right:0;padding-top:0;padding-bottom:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-right-radius:4px;border-top-left-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:6px;margin-bottom:6px}.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-left:15px;margin-right:15px}}@media (min-width:768px){.navbar-left{float:left !important}.navbar-right{float:right !important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#3a3f44;border-color:#2b2e32}.navbar-default .navbar-brand{color:#c8c8c8}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#ffffff;background-color:none}.navbar-default .navbar-text{color:#c8c8c8}.navbar-default .navbar-nav>li>a{color:#c8c8c8}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#ffffff;background-color:#272b2e}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#ffffff;background-color:#272b2e}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#cccccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#272b2e}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#272b2e}.navbar-default .navbar-toggle .icon-bar{background-color:#c8c8c8}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#2b2e32}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{background-color:#272b2e;color:#ffffff}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#c8c8c8}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#ffffff;background-color:#272b2e}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#ffffff;background-color:#272b2e}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#cccccc;background-color:transparent}}.navbar-default .navbar-link{color:#c8c8c8}.navbar-default .navbar-link:hover{color:#ffffff}.navbar-default .btn-link{color:#c8c8c8}.navbar-default .btn-link:hover,.navbar-default .btn-link:focus{color:#ffffff}.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:hover,.navbar-default .btn-link[disabled]:focus,fieldset[disabled] .navbar-default .btn-link:focus{color:#cccccc}.navbar-inverse{background-color:#7a8288;border-color:#62686d}.navbar-inverse .navbar-brand{color:#cccccc}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#ffffff;background-color:none}.navbar-inverse .navbar-text{color:#cccccc}.navbar-inverse .navbar-nav>li>a{color:#cccccc}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#ffffff;background-color:#5d6368}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#ffffff;background-color:#5d6368}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#cccccc;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#5d6368}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#5d6368}.navbar-inverse .navbar-toggle .icon-bar{background-color:#ffffff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#697075}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{background-color:#5d6368;color:#ffffff}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#62686d}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#62686d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#cccccc}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#ffffff;background-color:#5d6368}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#ffffff;background-color:#5d6368}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#cccccc;background-color:transparent}}.navbar-inverse .navbar-link{color:#cccccc}.navbar-inverse .navbar-link:hover{color:#ffffff}.navbar-inverse .btn-link{color:#cccccc}.navbar-inverse .btn-link:hover,.navbar-inverse .btn-link:focus{color:#ffffff}.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:hover,.navbar-inverse .btn-link[disabled]:focus,fieldset[disabled] .navbar-inverse .btn-link:focus{color:#cccccc}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:transparent;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{content:"/\00a0";padding:0 5px;color:#cccccc}.breadcrumb>.active{color:#7a8288}.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:8px 12px;line-height:1.42857143;text-decoration:none;color:#ffffff;background-color:#3a3f44;border:1px solid rgba(0,0,0,0.6);margin-left:-1px}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-bottom-right-radius:4px;border-top-right-radius:4px}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{z-index:2;color:#ffffff;background-color:transparent;border-color:rgba(0,0,0,0.6)}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:3;color:#ffffff;background-color:#232628;border-color:rgba(0,0,0,0.6);cursor:default}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#7a8288;background-color:#ffffff;border-color:rgba(0,0,0,0.6);cursor:not-allowed}.pagination-lg>li>a,.pagination-lg>li>span{padding:14px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-bottom-right-radius:6px;border-top-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-bottom-right-radius:3px;border-top-right-radius:3px}.pager{padding-left:0;margin:20px 0;list-style:none;text-align:center}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#3a3f44;border:1px solid rgba(0,0,0,0.6);border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:transparent}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#7a8288;background-color:#3a3f44;cursor:not-allowed}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:bold;line-height:1;color:#ffffff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:hover,a.label:focus{color:#ffffff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#3a3f44}.label-default[href]:hover,.label-default[href]:focus{background-color:#232628}.label-primary{background-color:#7a8288}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#62686d}.label-success{background-color:#62c462}.label-success[href]:hover,.label-success[href]:focus{background-color:#42b142}.label-info{background-color:#5bc0de}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#f89406}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#c67605}.label-danger{background-color:#ee5f5b}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#e9322d}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:bold;color:#ffffff;line-height:1;vertical-align:middle;white-space:nowrap;text-align:center;background-color:#7a8288;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge,.btn-group-xs>.btn .badge{top:0;padding:1px 5px}a.badge:hover,a.badge:focus{color:#ffffff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#ffffff;background-color:#7a8288}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#1c1e22}.jumbotron h1,.jumbotron .h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#050506}.container .jumbotron,.container-fluid .jumbotron{border-radius:6px;padding-left:15px;padding-right:15px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-left:60px;padding-right:60px}.jumbotron h1,.jumbotron .h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#1c1e22;border:1px solid #0c0d0e;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail>img,.thumbnail a>img{margin-left:auto;margin-right:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#ffffff}.thumbnail .caption{padding:9px;color:#c8c8c8}.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:bold}.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{background-color:#62c462;border-color:#62bd4f;color:#ffffff}.alert-success hr{border-top-color:#55b142}.alert-success .alert-link{color:#e6e6e6}.alert-info{background-color:#5bc0de;border-color:#3dced8;color:#ffffff}.alert-info hr{border-top-color:#2ac7d2}.alert-info .alert-link{color:#e6e6e6}.alert-warning{background-color:#f89406;border-color:#e96506;color:#ffffff}.alert-warning hr{border-top-color:#d05a05}.alert-warning .alert-link{color:#e6e6e6}.alert-danger{background-color:#ee5f5b;border-color:#ed4d63;color:#ffffff}.alert-danger hr{border-top-color:#ea364f}.alert-danger .alert-link{color:#e6e6e6}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{overflow:hidden;height:20px;margin-bottom:20px;background-color:#1c1e22;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress-bar{float:left;width:0%;height:100%;font-size:12px;line-height:20px;color:#ffffff;text-align:center;background-color:#7a8288;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-transition:width 0.6s ease;-o-transition:width 0.6s ease;transition:width 0.6s ease}.progress-striped .progress-bar,.progress-bar-striped{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress.active .progress-bar,.progress-bar.active{-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-success{background-color:#62c462}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-warning{background-color:#f89406}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-danger{background-color:#ee5f5b}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{zoom:1;overflow:hidden}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-left,.media-right,.media-body{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{margin-bottom:20px;padding-left:0}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#32383e;border:1px solid rgba(0,0,0,0.6)}.list-group-item:first-child{border-top-right-radius:4px;border-top-left-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item,button.list-group-item{color:#c8c8c8}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#ffffff}a.list-group-item:hover,button.list-group-item:hover,a.list-group-item:focus,button.list-group-item:focus{text-decoration:none;color:#c8c8c8;background-color:#3e444c}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:hover,.list-group-item.disabled:focus{background-color:#999999;color:#7a8288;cursor:not-allowed}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text{color:#7a8288}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{z-index:2;color:#ffffff;background-color:#3e444c;border-color:rgba(0,0,0,0.6)}.list-group-item.active .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>.small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:hover .list-group-item-text,.list-group-item.active:focus .list-group-item-text{color:#a2aab4}.list-group-item-success{color:#ffffff;background-color:#62c462}a.list-group-item-success,button.list-group-item-success{color:#ffffff}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,button.list-group-item-success:hover,a.list-group-item-success:focus,button.list-group-item-success:focus{color:#ffffff;background-color:#4fbd4f}a.list-group-item-success.active,button.list-group-item-success.active,a.list-group-item-success.active:hover,button.list-group-item-success.active:hover,a.list-group-item-success.active:focus,button.list-group-item-success.active:focus{color:#fff;background-color:#ffffff;border-color:#ffffff}.list-group-item-info{color:#ffffff;background-color:#5bc0de}a.list-group-item-info,button.list-group-item-info{color:#ffffff}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,button.list-group-item-info:hover,a.list-group-item-info:focus,button.list-group-item-info:focus{color:#ffffff;background-color:#46b8da}a.list-group-item-info.active,button.list-group-item-info.active,a.list-group-item-info.active:hover,button.list-group-item-info.active:hover,a.list-group-item-info.active:focus,button.list-group-item-info.active:focus{color:#fff;background-color:#ffffff;border-color:#ffffff}.list-group-item-warning{color:#ffffff;background-color:#f89406}a.list-group-item-warning,button.list-group-item-warning{color:#ffffff}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,button.list-group-item-warning:hover,a.list-group-item-warning:focus,button.list-group-item-warning:focus{color:#ffffff;background-color:#df8505}a.list-group-item-warning.active,button.list-group-item-warning.active,a.list-group-item-warning.active:hover,button.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus,button.list-group-item-warning.active:focus{color:#fff;background-color:#ffffff;border-color:#ffffff}.list-group-item-danger{color:#ffffff;background-color:#ee5f5b}a.list-group-item-danger,button.list-group-item-danger{color:#ffffff}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,button.list-group-item-danger:hover,a.list-group-item-danger:focus,button.list-group-item-danger:focus{color:#ffffff;background-color:#ec4844}a.list-group-item-danger.active,button.list-group-item-danger.active,a.list-group-item-danger.active:hover,button.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus,button.list-group-item-danger.active:focus{color:#fff;background-color:#ffffff;border-color:#ffffff}.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:#2e3338;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.05);box-shadow:0 1px 1px rgba(0,0,0,0.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:3px;border-top-left-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>a,.panel-title>small,.panel-title>.small,.panel-title>small>a,.panel-title>.small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#3e444c;border-top:1px solid rgba(0,0,0,0.6);border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-right-radius:3px;border-top-left-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-right-radius:0;border-top-left-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.table,.panel>.table-responsive>.table,.panel>.panel-collapse>.table{margin-bottom:0}.panel>.table caption,.panel>.table-responsive>.table caption,.panel>.panel-collapse>.table caption{padding-left:15px;padding-right:15px}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-right-radius:3px;border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.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 td:first-child,.panel>.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 td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th: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 th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.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 td:last-child,.panel>.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 td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th: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 th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table:last-child,.panel>.table-responsive:last-child>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-left-radius:3px;border-bottom-right-radius:3px}.panel>.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 td:first-child,.panel>.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 td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.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 td:last-child,.panel>.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 td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.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,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #1c1e22}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{border:0;margin-bottom: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,.panel-group .panel-heading+.panel-collapse>.list-group{border-top:1px solid rgba(0,0,0,0.6)}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid rgba(0,0,0,0.6)}.panel-default{border-color:rgba(0,0,0,0.6)}.panel-default>.panel-heading{color:#c8c8c8;background-color:#3e444c;border-color:rgba(0,0,0,0.6)}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:rgba(0,0,0,0.6)}.panel-default>.panel-heading .badge{color:#3e444c;background-color:#c8c8c8}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:rgba(0,0,0,0.6)}.panel-primary{border-color:rgba(0,0,0,0.6)}.panel-primary>.panel-heading{color:#ffffff;background-color:#7a8288;border-color:rgba(0,0,0,0.6)}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:rgba(0,0,0,0.6)}.panel-primary>.panel-heading .badge{color:#7a8288;background-color:#ffffff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:rgba(0,0,0,0.6)}.panel-success{border-color:rgba(0,0,0,0.6)}.panel-success>.panel-heading{color:#ffffff;background-color:#62c462;border-color:rgba(0,0,0,0.6)}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:rgba(0,0,0,0.6)}.panel-success>.panel-heading .badge{color:#62c462;background-color:#ffffff}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:rgba(0,0,0,0.6)}.panel-info{border-color:rgba(0,0,0,0.6)}.panel-info>.panel-heading{color:#ffffff;background-color:#5bc0de;border-color:rgba(0,0,0,0.6)}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:rgba(0,0,0,0.6)}.panel-info>.panel-heading .badge{color:#5bc0de;background-color:#ffffff}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:rgba(0,0,0,0.6)}.panel-warning{border-color:rgba(0,0,0,0.6)}.panel-warning>.panel-heading{color:#ffffff;background-color:#f89406;border-color:rgba(0,0,0,0.6)}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:rgba(0,0,0,0.6)}.panel-warning>.panel-heading .badge{color:#f89406;background-color:#ffffff}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:rgba(0,0,0,0.6)}.panel-danger{border-color:rgba(0,0,0,0.6)}.panel-danger>.panel-heading{color:#ffffff;background-color:#ee5f5b;border-color:rgba(0,0,0,0.6)}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:rgba(0,0,0,0.6)}.panel-danger>.panel-heading .badge{color:#ee5f5b;background-color:#ffffff}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:rgba(0,0,0,0.6)}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object,.embed-responsive video{position:absolute;top:0;left:0;bottom:0;height:100%;width:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#1c1e22;border:1px solid #0c0d0e;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,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:bold;line-height:1;color:#000000;text-shadow:0 1px 0 #ffffff;opacity:0.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000000;text-decoration:none;cursor:pointer;opacity:0.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{display:none;overflow:hidden;position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0, -25%);-ms-transform:translate(0, -25%);-o-transform:translate(0, -25%);transform:translate(0, -25%);-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#2e3338;border:1px solid #999999;border:1px solid rgba(0,0,0,0.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,0.5);box-shadow:0 3px 9px rgba(0,0,0,0.5);-webkit-background-clip:padding-box;background-clip:padding-box;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:0.5;filter:alpha(opacity=50)}.modal-header{padding:15px;border-bottom:1px solid #1c1e22}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:20px}.modal-footer{padding:20px;text-align:right;border-top:1px solid #1c1e22}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.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,0.5);box-shadow:0 5px 15px rgba(0,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-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:normal;letter-spacing:normal;line-break:auto;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;word-wrap:normal;font-size:12px;opacity:0;filter:alpha(opacity=0)}.tooltip.in{opacity:0.9;filter:alpha(opacity=90)}.tooltip.top{margin-top:-3px;padding:5px 0}.tooltip.right{margin-left:3px;padding:0 5px}.tooltip.bottom{margin-top:3px;padding:5px 0}.tooltip.left{margin-left:-3px;padding:0 5px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#ffffff;text-align:center;background-color:#000000;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:#000000}.tooltip.top-left .tooltip-arrow{bottom:0;right:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:normal;letter-spacing:normal;line-break:auto;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;word-wrap:normal;font-size:14px;background-color:#2e3338;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #999999;border:1px solid rgba(0,0,0,0.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,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{margin:0;padding:8px 14px;font-size:14px;background-color:#2e3338;border-bottom:1px solid #22262a;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{border-width:10px;content:""}.popover.top>.arrow{left:50%;margin-left:-11px;border-bottom-width:0;border-top-color:#666666;border-top-color:rgba(0,0,0,0.25);bottom:-11px}.popover.top>.arrow:after{content:" ";bottom:1px;margin-left:-10px;border-bottom-width:0;border-top-color:#2e3338}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-left-width:0;border-right-color:#666666;border-right-color:rgba(0,0,0,0.25)}.popover.right>.arrow:after{content:" ";left:1px;bottom:-10px;border-left-width:0;border-right-color:#2e3338}.popover.bottom>.arrow{left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#666666;border-bottom-color:rgba(0,0,0,0.25);top:-11px}.popover.bottom>.arrow:after{content:" ";top:1px;margin-left:-10px;border-top-width:0;border-bottom-color:#2e3338}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#666666;border-left-color:rgba(0,0,0,0.25)}.popover.left>.arrow:after{content:" ";right:1px;border-right-width:0;border-left-color:#2e3338;bottom:-10px}.carousel{position:relative}.carousel-inner{position:relative;overflow:hidden;width:100%}.carousel-inner>.item{display:none;position:relative;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.next,.carousel-inner>.item.active.right{-webkit-transform:translate3d(100%, 0, 0);transform:translate3d(100%, 0, 0);left:0}.carousel-inner>.item.prev,.carousel-inner>.item.active.left{-webkit-transform:translate3d(-100%, 0, 0);transform:translate3d(-100%, 0, 0);left:0}.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right,.carousel-inner>.item.active{-webkit-transform:translate3d(0, 0, 0);transform:translate3d(0, 0, 0);left:0}}.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;left:0;bottom:0;width:15%;opacity:0.5;filter:alpha(opacity=50);font-size:20px;color:#ffffff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6);background-color:rgba(0,0,0,0)}.carousel-control.left{background-image:-webkit-linear-gradient(left, rgba(0,0,0,0.5) 0, rgba(0,0,0,0.0001) 100%);background-image:-o-linear-gradient(left, rgba(0,0,0,0.5) 0, rgba(0,0,0,0.0001) 100%);background-image:-webkit-gradient(linear, left top, right top, from(rgba(0,0,0,0.5)), to(rgba(0,0,0,0.0001)));background-image:linear-gradient(to right, rgba(0,0,0,0.5) 0, rgba(0,0,0,0.0001) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1)}.carousel-control.right{left:auto;right:0;background-image:-webkit-linear-gradient(left, rgba(0,0,0,0.0001) 0, rgba(0,0,0,0.5) 100%);background-image:-o-linear-gradient(left, rgba(0,0,0,0.0001) 0, rgba(0,0,0,0.5) 100%);background-image:-webkit-gradient(linear, left top, right top, from(rgba(0,0,0,0.0001)), to(rgba(0,0,0,0.5)));background-image:linear-gradient(to right, rgba(0,0,0,0.0001) 0, rgba(0,0,0,0.5) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1)}.carousel-control:hover,.carousel-control:focus{outline:0;color:#ffffff;text-decoration:none;opacity:0.9;filter:alpha(opacity=90)}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;margin-top:-10px;z-index:5;display:inline-block}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%;margin-left:-10px}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%;margin-right:-10px}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;line-height:1;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%;margin-left:-30%;padding-left:0;list-style:none;text-align:center}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;border:1px solid #ffffff;border-radius:10px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0)}.carousel-indicators .active{margin:0;width:12px;height:12px;background-color:#ffffff}.carousel-caption{position:absolute;left:15%;right:15%;bottom:20px;z-index:10;padding-top:20px;padding-bottom:20px;color:#ffffff;text-align:center;text-shadow:0 1px 2px rgba(0,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-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{left:20%;right:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.clearfix:after,.dl-horizontal dd:before,.dl-horizontal dd:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after,.form-horizontal .form-group:before,.form-horizontal .form-group:after,.btn-toolbar:before,.btn-toolbar:after,.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after,.nav:before,.nav:after,.navbar:before,.navbar:after,.navbar-header:before,.navbar-header:after,.navbar-collapse:before,.navbar-collapse:after,.pager:before,.pager:after,.panel-body:before,.panel-body:after,.modal-header:before,.modal-header:after,.modal-footer:before,.modal-footer:after{content:" ";display:table}.clearfix:after,.dl-horizontal dd:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.nav:after,.navbar:after,.navbar-header:after,.navbar-collapse:after,.pager:after,.panel-body:after,.modal-header:after,.modal-footer:after{clear:both}.center-block{display:block;margin-left:auto;margin-right: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}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none !important}.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block{display:none !important}@media (max-width:767px){.visible-xs{display:block !important}table.visible-xs{display:table !important}tr.visible-xs{display:table-row !important}th.visible-xs,td.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 !important}tr.visible-sm{display:table-row !important}th.visible-sm,td.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 !important}tr.visible-md{display:table-row !important}th.visible-md,td.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 !important}tr.visible-lg{display:table-row !important}th.visible-lg,td.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 !important}tr.visible-print{display:table-row !important}th.visible-print,td.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}}.navbar{background-image:-webkit-linear-gradient(#484e55, #3a3f44 60%, #313539);background-image:-o-linear-gradient(#484e55, #3a3f44 60%, #313539);background-image:-webkit-gradient(linear, left top, left bottom, from(#484e55), color-stop(60%, #3a3f44), to(#313539));background-image:linear-gradient(#484e55, #3a3f44 60%, #313539);background-repeat:no-repeat;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff484e55', endColorstr='#ff313539', GradientType=0);-webkit-filter:none;filter:none;border:1px solid rgba(0,0,0,0.6);text-shadow:1px 1px 1px rgba(0,0,0,0.3)}.navbar-inverse{background-image:-webkit-linear-gradient(#8a9196, #7a8288 60%, #70787d);background-image:-o-linear-gradient(#8a9196, #7a8288 60%, #70787d);background-image:-webkit-gradient(linear, left top, left bottom, from(#8a9196), color-stop(60%, #7a8288), to(#70787d));background-image:linear-gradient(#8a9196, #7a8288 60%, #70787d);background-repeat:no-repeat;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff8a9196', endColorstr='#ff70787d', GradientType=0);-webkit-filter:none;filter:none}.navbar-inverse .badge{background-color:#5d6368}.navbar-nav>li>a{border-right:1px solid rgba(0,0,0,0.2);border-left:1px solid rgba(255,255,255,0.1)}.navbar-nav>li>a:hover{background-image:-webkit-linear-gradient(#020202, #101112 40%, #191b1d);background-image:-o-linear-gradient(#020202, #101112 40%, #191b1d);background-image:-webkit-gradient(linear, left top, left bottom, from(#020202), color-stop(40%, #101112), to(#191b1d));background-image:linear-gradient(#020202, #101112 40%, #191b1d);background-repeat:no-repeat;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff020202', endColorstr='#ff191b1d', GradientType=0);-webkit-filter:none;filter:none;border-left-color:transparent}.navbar .nav .open>a{border-color:transparent}.navbar-nav>li.active>a{border-left-color:transparent}.navbar-form{margin-left:5px;margin-right:5px}.btn,.btn:hover{border-color:rgba(0,0,0,0.6);text-shadow:1px 1px 1px rgba(0,0,0,0.3)}.btn-default{background-image:-webkit-linear-gradient(#484e55, #3a3f44 60%, #313539);background-image:-o-linear-gradient(#484e55, #3a3f44 60%, #313539);background-image:-webkit-gradient(linear, left top, left bottom, from(#484e55), color-stop(60%, #3a3f44), to(#313539));background-image:linear-gradient(#484e55, #3a3f44 60%, #313539);background-repeat:no-repeat;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff484e55', endColorstr='#ff313539', GradientType=0);-webkit-filter:none;filter:none}.btn-default:hover{background-image:-webkit-linear-gradient(#020202, #101112 40%, #191b1d);background-image:-o-linear-gradient(#020202, #101112 40%, #191b1d);background-image:-webkit-gradient(linear, left top, left bottom, from(#020202), color-stop(40%, #101112), to(#191b1d));background-image:linear-gradient(#020202, #101112 40%, #191b1d);background-repeat:no-repeat;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff020202', endColorstr='#ff191b1d', GradientType=0);-webkit-filter:none;filter:none}.btn-primary{background-image:-webkit-linear-gradient(#8a9196, #7a8288 60%, #70787d);background-image:-o-linear-gradient(#8a9196, #7a8288 60%, #70787d);background-image:-webkit-gradient(linear, left top, left bottom, from(#8a9196), color-stop(60%, #7a8288), to(#70787d));background-image:linear-gradient(#8a9196, #7a8288 60%, #70787d);background-repeat:no-repeat;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff8a9196', endColorstr='#ff70787d', GradientType=0);-webkit-filter:none;filter:none}.btn-primary:hover{background-image:-webkit-linear-gradient(#404448, #4e5458 40%, #585e62);background-image:-o-linear-gradient(#404448, #4e5458 40%, #585e62);background-image:-webkit-gradient(linear, left top, left bottom, from(#404448), color-stop(40%, #4e5458), to(#585e62));background-image:linear-gradient(#404448, #4e5458 40%, #585e62);background-repeat:no-repeat;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff404448', endColorstr='#ff585e62', GradientType=0);-webkit-filter:none;filter:none}.btn-success{background-image:-webkit-linear-gradient(#78cc78, #62c462 60%, #53be53);background-image:-o-linear-gradient(#78cc78, #62c462 60%, #53be53);background-image:-webkit-gradient(linear, left top, left bottom, from(#78cc78), color-stop(60%, #62c462), to(#53be53));background-image:linear-gradient(#78cc78, #62c462 60%, #53be53);background-repeat:no-repeat;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff78cc78', endColorstr='#ff53be53', GradientType=0);-webkit-filter:none;filter:none}.btn-success:hover{background-image:-webkit-linear-gradient(#2f7d2f, #379337 40%, #3da23d);background-image:-o-linear-gradient(#2f7d2f, #379337 40%, #3da23d);background-image:-webkit-gradient(linear, left top, left bottom, from(#2f7d2f), color-stop(40%, #379337), to(#3da23d));background-image:linear-gradient(#2f7d2f, #379337 40%, #3da23d);background-repeat:no-repeat;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff2f7d2f', endColorstr='#ff3da23d', GradientType=0);-webkit-filter:none;filter:none}.btn-info{background-image:-webkit-linear-gradient(#74cae3, #5bc0de 60%, #4ab9db);background-image:-o-linear-gradient(#74cae3, #5bc0de 60%, #4ab9db);background-image:-webkit-gradient(linear, left top, left bottom, from(#74cae3), color-stop(60%, #5bc0de), to(#4ab9db));background-image:linear-gradient(#74cae3, #5bc0de 60%, #4ab9db);background-repeat:no-repeat;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff74cae3', endColorstr='#ff4ab9db', GradientType=0);-webkit-filter:none;filter:none}.btn-info:hover{background-image:-webkit-linear-gradient(#20829f, #2596b8 40%, #28a4c9);background-image:-o-linear-gradient(#20829f, #2596b8 40%, #28a4c9);background-image:-webkit-gradient(linear, left top, left bottom, from(#20829f), color-stop(40%, #2596b8), to(#28a4c9));background-image:linear-gradient(#20829f, #2596b8 40%, #28a4c9);background-repeat:no-repeat;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff20829f', endColorstr='#ff28a4c9', GradientType=0);-webkit-filter:none;filter:none}.btn-warning{background-image:-webkit-linear-gradient(#faa123, #f89406 60%, #e48806);background-image:-o-linear-gradient(#faa123, #f89406 60%, #e48806);background-image:-webkit-gradient(linear, left top, left bottom, from(#faa123), color-stop(60%, #f89406), to(#e48806));background-image:linear-gradient(#faa123, #f89406 60%, #e48806);background-repeat:no-repeat;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffaa123', endColorstr='#ffe48806', GradientType=0);-webkit-filter:none;filter:none}.btn-warning:hover{background-image:-webkit-linear-gradient(#804d03, #9e5f04 40%, #b26a04);background-image:-o-linear-gradient(#804d03, #9e5f04 40%, #b26a04);background-image:-webkit-gradient(linear, left top, left bottom, from(#804d03), color-stop(40%, #9e5f04), to(#b26a04));background-image:linear-gradient(#804d03, #9e5f04 40%, #b26a04);background-repeat:no-repeat;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff804d03', endColorstr='#ffb26a04', GradientType=0);-webkit-filter:none;filter:none}.btn-danger{background-image:-webkit-linear-gradient(#f17a77, #ee5f5b 60%, #ec4d49);background-image:-o-linear-gradient(#f17a77, #ee5f5b 60%, #ec4d49);background-image:-webkit-gradient(linear, left top, left bottom, from(#f17a77), color-stop(60%, #ee5f5b), to(#ec4d49));background-image:linear-gradient(#f17a77, #ee5f5b 60%, #ec4d49);background-repeat:no-repeat;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff17a77', endColorstr='#ffec4d49', GradientType=0);-webkit-filter:none;filter:none}.btn-danger:hover{background-image:-webkit-linear-gradient(#bb1813, #d71c16 40%, #e7201a);background-image:-o-linear-gradient(#bb1813, #d71c16 40%, #e7201a);background-image:-webkit-gradient(linear, left top, left bottom, from(#bb1813), color-stop(40%, #d71c16), to(#e7201a));background-image:linear-gradient(#bb1813, #d71c16 40%, #e7201a);background-repeat:no-repeat;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffbb1813', endColorstr='#ffe7201a', GradientType=0);-webkit-filter:none;filter:none}.btn-link,.btn-link:hover{border-color:transparent}h1,h2,h3,h4,h5,h6{text-shadow:-1px -1px 0 rgba(0,0,0,0.3)}.text-primary,.text-primary:hover{color:#7a8288}.text-success,.text-success:hover{color:#62c462}.text-danger,.text-danger:hover{color:#ee5f5b}.text-warning,.text-warning:hover{color:#f89406}.text-info,.text-info:hover{color:#5bc0de}.table .success,.table .warning,.table .danger,.table .info{color:#fff}.table-bordered tbody tr.success td,.table-bordered tbody tr.warning td,.table-bordered tbody tr.danger td,.table-bordered tbody tr.success:hover td,.table-bordered tbody tr.warning:hover td,.table-bordered tbody tr.danger:hover td{border-color:#1c1e22}.table-responsive>.table{background-color:#2e3338}input,textarea{color:#272b30}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline,.has-warning.radio label,.has-warning.checkbox label,.has-warning.radio-inline label,.has-warning.checkbox-inline label,.has-warning .form-control-feedback{color:#f89406}.has-warning .form-control,.has-warning .form-control:focus{border-color:#f89406}.has-warning .input-group-addon{background-color:#272b30;border:none}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline,.has-error.radio label,.has-error.checkbox label,.has-error.radio-inline label,.has-error.checkbox-inline label,.has-error .form-control-feedback{color:#ee5f5b}.has-error .form-control,.has-error .form-control:focus{border-color:#ee5f5b}.has-error .input-group-addon{background-color:#272b30;border:none}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline,.has-success.radio label,.has-success.checkbox label,.has-success.radio-inline label,.has-success.checkbox-inline label,.has-success .form-control-feedback{color:#62c462}.has-success .form-control,.has-success .form-control:focus{border-color:#62c462}.has-success .input-group-addon{background-color:#272b30;border:none}legend{color:#fff}.input-group-addon{border-color:rgba(0,0,0,0.6);text-shadow:1px 1px 1px rgba(0,0,0,0.3);background-image:-webkit-linear-gradient(#484e55, #3a3f44 60%, #313539);background-image:-o-linear-gradient(#484e55, #3a3f44 60%, #313539);background-image:-webkit-gradient(linear, left top, left bottom, from(#484e55), color-stop(60%, #3a3f44), to(#313539));background-image:linear-gradient(#484e55, #3a3f44 60%, #313539);background-repeat:no-repeat;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff484e55', endColorstr='#ff313539', GradientType=0);-webkit-filter:none;filter:none;color:#ffffff}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{border-color:rgba(0,0,0,0.6)}.nav-pills>li>a{background-image:-webkit-linear-gradient(#484e55, #3a3f44 60%, #313539);background-image:-o-linear-gradient(#484e55, #3a3f44 60%, #313539);background-image:-webkit-gradient(linear, left top, left bottom, from(#484e55), color-stop(60%, #3a3f44), to(#313539));background-image:linear-gradient(#484e55, #3a3f44 60%, #313539);background-repeat:no-repeat;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff484e55', endColorstr='#ff313539', GradientType=0);-webkit-filter:none;filter:none;border:1px solid rgba(0,0,0,0.6);text-shadow:1px 1px 1px rgba(0,0,0,0.3)}.nav-pills>li>a:hover{background-image:-webkit-linear-gradient(#020202, #101112 40%, #191b1d);background-image:-o-linear-gradient(#020202, #101112 40%, #191b1d);background-image:-webkit-gradient(linear, left top, left bottom, from(#020202), color-stop(40%, #101112), to(#191b1d));background-image:linear-gradient(#020202, #101112 40%, #191b1d);background-repeat:no-repeat;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff020202', endColorstr='#ff191b1d', GradientType=0);-webkit-filter:none;filter:none;border:1px solid rgba(0,0,0,0.6)}.nav-pills>li.active>a,.nav-pills>li.active>a:hover{background-color:none;background-image:-webkit-linear-gradient(#020202, #101112 40%, #191b1d);background-image:-o-linear-gradient(#020202, #101112 40%, #191b1d);background-image:-webkit-gradient(linear, left top, left bottom, from(#020202), color-stop(40%, #101112), to(#191b1d));background-image:linear-gradient(#020202, #101112 40%, #191b1d);background-repeat:no-repeat;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff020202', endColorstr='#ff191b1d', GradientType=0);-webkit-filter:none;filter:none;border:1px solid rgba(0,0,0,0.6)}.nav-pills>li.disabled>a,.nav-pills>li.disabled>a:hover{background-image:-webkit-linear-gradient(#484e55, #3a3f44 60%, #313539);background-image:-o-linear-gradient(#484e55, #3a3f44 60%, #313539);background-image:-webkit-gradient(linear, left top, left bottom, from(#484e55), color-stop(60%, #3a3f44), to(#313539));background-image:linear-gradient(#484e55, #3a3f44 60%, #313539);background-repeat:no-repeat;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff484e55', endColorstr='#ff313539', GradientType=0);-webkit-filter:none;filter:none}.pagination>li>a,.pagination>li>span{text-shadow:1px 1px 1px rgba(0,0,0,0.3);background-image:-webkit-linear-gradient(#484e55, #3a3f44 60%, #313539);background-image:-o-linear-gradient(#484e55, #3a3f44 60%, #313539);background-image:-webkit-gradient(linear, left top, left bottom, from(#484e55), color-stop(60%, #3a3f44), to(#313539));background-image:linear-gradient(#484e55, #3a3f44 60%, #313539);background-repeat:no-repeat;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff484e55', endColorstr='#ff313539', GradientType=0);-webkit-filter:none;filter:none}.pagination>li>a:hover,.pagination>li>span:hover{background-image:-webkit-linear-gradient(#020202, #101112 40%, #191b1d);background-image:-o-linear-gradient(#020202, #101112 40%, #191b1d);background-image:-webkit-gradient(linear, left top, left bottom, from(#020202), color-stop(40%, #101112), to(#191b1d));background-image:linear-gradient(#020202, #101112 40%, #191b1d);background-repeat:no-repeat;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff020202', endColorstr='#ff191b1d', GradientType=0);-webkit-filter:none;filter:none}.pagination>li.active>a,.pagination>li.active>span{background-image:-webkit-linear-gradient(#020202, #101112 40%, #191b1d);background-image:-o-linear-gradient(#020202, #101112 40%, #191b1d);background-image:-webkit-gradient(linear, left top, left bottom, from(#020202), color-stop(40%, #101112), to(#191b1d));background-image:linear-gradient(#020202, #101112 40%, #191b1d);background-repeat:no-repeat;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff020202', endColorstr='#ff191b1d', GradientType=0);-webkit-filter:none;filter:none}.pagination>li.disabled>a,.pagination>li.disabled>a:hover,.pagination>li.disabled>span,.pagination>li.disabled>span:hover{background-color:transparent;background-image:-webkit-linear-gradient(#484e55, #3a3f44 60%, #313539);background-image:-o-linear-gradient(#484e55, #3a3f44 60%, #313539);background-image:-webkit-gradient(linear, left top, left bottom, from(#484e55), color-stop(60%, #3a3f44), to(#313539));background-image:linear-gradient(#484e55, #3a3f44 60%, #313539);background-repeat:no-repeat;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff484e55', endColorstr='#ff313539', GradientType=0);-webkit-filter:none;filter:none}.pager>li>a{background-image:-webkit-linear-gradient(#484e55, #3a3f44 60%, #313539);background-image:-o-linear-gradient(#484e55, #3a3f44 60%, #313539);background-image:-webkit-gradient(linear, left top, left bottom, from(#484e55), color-stop(60%, #3a3f44), to(#313539));background-image:linear-gradient(#484e55, #3a3f44 60%, #313539);background-repeat:no-repeat;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff484e55', endColorstr='#ff313539', GradientType=0);-webkit-filter:none;filter:none;text-shadow:1px 1px 1px rgba(0,0,0,0.3)}.pager>li>a:hover{background-image:-webkit-linear-gradient(#020202, #101112 40%, #191b1d);background-image:-o-linear-gradient(#020202, #101112 40%, #191b1d);background-image:-webkit-gradient(linear, left top, left bottom, from(#020202), color-stop(40%, #101112), to(#191b1d));background-image:linear-gradient(#020202, #101112 40%, #191b1d);background-repeat:no-repeat;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff020202', endColorstr='#ff191b1d', GradientType=0);-webkit-filter:none;filter:none}.pager>li.disabled>a,.pager>li.disabled>a:hover{background-color:transparent;background-image:-webkit-linear-gradient(#484e55, #3a3f44 60%, #313539);background-image:-o-linear-gradient(#484e55, #3a3f44 60%, #313539);background-image:-webkit-gradient(linear, left top, left bottom, from(#484e55), color-stop(60%, #3a3f44), to(#313539));background-image:linear-gradient(#484e55, #3a3f44 60%, #313539);background-repeat:no-repeat;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff484e55', endColorstr='#ff313539', GradientType=0);-webkit-filter:none;filter:none}.breadcrumb{border:1px solid rgba(0,0,0,0.6);text-shadow:1px 1px 1px rgba(0,0,0,0.3);background-image:-webkit-linear-gradient(#484e55, #3a3f44 60%, #313539);background-image:-o-linear-gradient(#484e55, #3a3f44 60%, #313539);background-image:-webkit-gradient(linear, left top, left bottom, from(#484e55), color-stop(60%, #3a3f44), to(#313539));background-image:linear-gradient(#484e55, #3a3f44 60%, #313539);background-repeat:no-repeat;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff484e55', endColorstr='#ff313539', GradientType=0);-webkit-filter:none;filter:none}.alert .alert-link,.alert a{color:#fff;text-decoration:underline}.alert .close{color:#000000;text-decoration:none}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#0c0d0e}a.list-group-item.active,a.list-group-item.active:hover,a.list-group-item.active:focus{border-color:rgba(0,0,0,0.6)}a.list-group-item-success.active{background-color:#62c462}a.list-group-item-success.active:hover,a.list-group-item-success.active:focus{background-color:#4fbd4f}a.list-group-item-warning.active{background-color:#f89406}a.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus{background-color:#df8505}a.list-group-item-danger.active{background-color:#ee5f5b}a.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus{background-color:#ec4844}.jumbotron{border:1px solid rgba(0,0,0,0.6)}.panel-primary .panel-heading,.panel-success .panel-heading,.panel-danger .panel-heading,.panel-warning .panel-heading,.panel-info .panel-heading{border-color:#000}
\ No newline at end of file
diff --git a/admin/static/fonts/FantasqueSansMono-Regular.woff b/admin/static/fonts/FantasqueSansMono-Regular.woff
new file mode 100644
index 00000000..c88cd830
Binary files /dev/null and b/admin/static/fonts/FantasqueSansMono-Regular.woff differ
diff --git a/admin/static/fonts/LinBiolinum_R.woff b/admin/static/fonts/LinBiolinum_R.woff
new file mode 100644
index 00000000..5c399fd8
Binary files /dev/null and b/admin/static/fonts/LinBiolinum_R.woff differ
diff --git a/admin/static/fonts/LinBiolinum_RB.woff b/admin/static/fonts/LinBiolinum_RB.woff
new file mode 100644
index 00000000..fbeead0f
Binary files /dev/null and b/admin/static/fonts/LinBiolinum_RB.woff differ
diff --git a/admin/static/fonts/LinBiolinum_RI.woff b/admin/static/fonts/LinBiolinum_RI.woff
new file mode 100644
index 00000000..40661f13
Binary files /dev/null and b/admin/static/fonts/LinBiolinum_RI.woff differ
diff --git a/admin/static/img/epita.png b/admin/static/img/epita.png
deleted file mode 100644
index e89f7181..00000000
Binary files a/admin/static/img/epita.png and /dev/null differ
diff --git a/admin/static/img/fic.png b/admin/static/img/fic.png
deleted file mode 100644
index 7956bd4d..00000000
Binary files a/admin/static/img/fic.png and /dev/null differ
diff --git a/admin/static/img/srs.png b/admin/static/img/srs.png
deleted file mode 100644
index 82294f52..00000000
Binary files a/admin/static/img/srs.png and /dev/null differ
diff --git a/admin/static/index.html b/admin/static/index.html
deleted file mode 100644
index 67c1f462..00000000
--- a/admin/static/index.html
+++ /dev/null
@@ -1,51 +0,0 @@
-
-
-
-
- Challenge Forensic - Administration
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ time.hours | time }}
- :
- {{ time.minutes | time }}
- :
- {{ time.seconds | time }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/admin/static/js/angular-resource.min.js b/admin/static/js/angular-resource.min.js
index c3fb7aab..8b924c37 100644
--- a/admin/static/js/angular-resource.min.js
+++ b/admin/static/js/angular-resource.min.js
@@ -1,14 +1,15 @@
/*
- AngularJS v1.4.8
- (c) 2010-2015 Google, Inc. http://angularjs.org
+ AngularJS v1.7.9
+ (c) 2010-2018 Google, Inc. http://angularjs.org
License: MIT
*/
-(function(I,f,C){'use strict';function D(t,e){e=e||{};f.forEach(e,function(f,k){delete e[k]});for(var k in t)!t.hasOwnProperty(k)||"$"===k.charAt(0)&&"$"===k.charAt(1)||(e[k]=t[k]);return e}var y=f.$$minErr("$resource"),B=/^(\.[a-zA-Z_$@][0-9a-zA-Z_$@]*)+$/;f.module("ngResource",["ng"]).provider("$resource",function(){var t=/^https?:\/\/[^\/]*/,e=this;this.defaults={stripTrailingSlashes:!0,actions:{get:{method:"GET"},save:{method:"POST"},query:{method:"GET",isArray:!0},remove:{method:"DELETE"},"delete":{method:"DELETE"}}};
-this.$get=["$http","$q",function(k,F){function w(f,g){this.template=f;this.defaults=r({},e.defaults,g);this.urlParams={}}function z(l,g,s,h){function c(a,q){var c={};q=r({},g,q);u(q,function(b,q){x(b)&&(b=b());var m;if(b&&b.charAt&&"@"==b.charAt(0)){m=a;var d=b.substr(1);if(null==d||""===d||"hasOwnProperty"===d||!B.test("."+d))throw y("badmember",d);for(var d=d.split("."),n=0,g=d.length;n/g,">")}function A(a){for(;a;){if(a.nodeType===s.Node.ELEMENT_NODE)for(var e=a.attributes,d=0,b=e.length;d"))},end:function(a){a=q(a);d||!0!==m[a]||!0===r[a]||(b(""),b(a),b(">"));a==d&&(d=!1)},chars:function(a){d||
+b(L(a))}}};J=s.Node.prototype.contains||function(a){return!!(this.compareDocumentPosition(a)&16)};var z=/[\uD800-\uDBFF][\uDC00-\uDFFF]/g,u=/([^#-~ |!])/g,r=f("area,br,col,hr,img,wbr"),x=f("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),p=f("rp,rt"),n=h({},p,x),x=h({},x,f("address,article,aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,section,table,ul")),p=h({},p,f("a,abbr,acronym,b,bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,samp,small,span,strike,strong,sub,sup,time,tt,u,var")),
+l=f("circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph,hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline,radialGradient,rect,stop,svg,switch,text,title,tspan"),w=f("script,style"),m=h({},r,x,p,n),O=f("background,cite,href,longdesc,src,xlink:href,xml:base"),n=f("abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,scope,scrolling,shape,size,span,start,summary,tabindex,target,title,type,valign,value,vspace,width"),
+p=f("accent-height,accumulate,additive,alphabetic,arabic-form,ascent,baseProfile,bbox,begin,by,calcMode,cap-height,class,color,color-rendering,content,cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,font-size,font-stretch,font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,gradientUnits,hanging,height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,keySplines,keyTimes,lang,marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mathematical,max,min,offset,opacity,orient,origin,overline-position,overline-thickness,panose-1,path,pathLength,points,preserveAspectRatio,r,refX,refY,repeatCount,repeatDur,requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,stemv,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,stroke,stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,underline-position,underline-thickness,unicode,unicode-range,units-per-em,values,version,viewBox,visibility,width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,xlink:show,xlink:title,xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,zoomAndPan",
+!0),M=h({},O,p,n),N=function(a,e){function d(b){b=" "+b;try{var d=(new a.DOMParser).parseFromString(b,"text/html").body;d.firstChild.remove();return d}catch(e){}}function b(a){c.innerHTML=a;e.documentMode&&A(c);return c}var g;if(e&&e.implementation)g=e.implementation.createHTMLDocument("inert");else throw D("noinert");var c=(g.documentElement||g.getDocumentElement()).querySelector("body");c.innerHTML=' ';return c.querySelector("svg")?
+(c.innerHTML=' ',c.querySelector("svg img")?d:b):function(b){b=" "+b;try{b=encodeURI(b)}catch(d){return}var e=new a.XMLHttpRequest;e.responseType="document";e.open("GET","data:text/html;charset=utf-8,"+b,!1);e.send(null);b=e.response.body;b.firstChild.remove();return b}}(s,s.document)}).info({angularVersion:"1.7.9"});c.module("ngSanitize").filter("linky",["$sanitize",function(f){var h=/((s?ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"\u201d\u2019]/i,
+t=/^mailto:/i,q=c.$$minErr("linky"),s=c.isDefined,A=c.isFunction,v=c.isObject,y=c.isString;return function(c,z,u){function r(c){c&&l.push(P(c))}function x(c,g){var f,a=p(c);l.push("');r(g);l.push(" ")}if(null==c||""===c)return c;if(!y(c))throw q("notstring",c);for(var p=A(u)?u:v(u)?function(){return u}:function(){return{}},n=c,l=[],w,m;c=n.match(h);)w=c[0],c[2]||
+c[4]||(w=(c[3]?"http://":"mailto:")+w),m=c.index,r(n.substr(0,m)),x(w,c[0].replace(t,"")),n=n.substring(m+c[0].length);r(n);return f(l.join(""))}}])})(window,window.angular);
+//# sourceMappingURL=angular-sanitize.min.js.map
diff --git a/admin/static/js/angular.min.js b/admin/static/js/angular.min.js
deleted file mode 120000
index 86b88964..00000000
--- a/admin/static/js/angular.min.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../frontend/static/js/angular.min.js
\ No newline at end of file
diff --git a/admin/static/js/angular.min.js b/admin/static/js/angular.min.js
new file mode 100644
index 00000000..f6bf3370
--- /dev/null
+++ b/admin/static/js/angular.min.js
@@ -0,0 +1,350 @@
+/*
+ AngularJS v1.7.9
+ (c) 2010-2018 Google, Inc. http://angularjs.org
+ License: MIT
+*/
+(function(C){'use strict';function re(a){if(D(a))w(a.objectMaxDepth)&&(Wb.objectMaxDepth=Xb(a.objectMaxDepth)?a.objectMaxDepth:NaN),w(a.urlErrorParamsEnabled)&&Ga(a.urlErrorParamsEnabled)&&(Wb.urlErrorParamsEnabled=a.urlErrorParamsEnabled);else return Wb}function Xb(a){return W(a)&&0c)return"...";var d=b.$$hashKey,f;if(H(a)){f=0;for(var g=a.length;f
").append(a).html();try{return a[0].nodeType===Pa?K(b):b.match(/^(<[^>]+>)/)[1].replace(/^<([\w-]+)/,function(a,b){return"<"+K(b)})}catch(d){return K(b)}}function Tc(a){try{return decodeURIComponent(a)}catch(b){}}function gc(a){var b={};r((a||"").split("&"),
+function(a){var c,e,f;a&&(e=a=a.replace(/\+/g,"%20"),c=a.indexOf("="),-1!==c&&(e=a.substring(0,c),f=a.substring(c+1)),e=Tc(e),w(e)&&(f=w(f)?Tc(f):!0,ta.call(b,e)?H(b[e])?b[e].push(f):b[e]=[b[e],f]:b[e]=f))});return b}function ye(a){var b=[];r(a,function(a,c){H(a)?r(a,function(a){b.push(ba(c,!0)+(!0===a?"":"="+ba(a,!0)))}):b.push(ba(c,!0)+(!0===a?"":"="+ba(a,!0)))});return b.length?b.join("&"):""}function hc(a){return ba(a,!0).replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+")}function ba(a,
+b){return encodeURIComponent(a).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%3B/gi,";").replace(/%20/g,b?"%20":"+")}function ze(a,b){var d,c,e=Qa.length;for(c=0;c protocol indicates an extension, document.location.href does not match."))}function Uc(a,b,d){D(d)||(d={});d=S({strictDi:!1},d);var c=function(){a=x(a);if(a.injector()){var c=a[0]===C.document?"document":za(a);throw pa("btstrpd",c.replace(/,"<").replace(/>/,">"));}b=b||[];b.unshift(["$provide",function(b){b.value("$rootElement",a)}]);d.debugInfoEnabled&&b.push(["$compileProvider",
+function(a){a.debugInfoEnabled(!0)}]);b.unshift("ng");c=fb(b,d.strictDi);c.invoke(["$rootScope","$rootElement","$compile","$injector",function(a,b,c,d){a.$apply(function(){b.data("$injector",d);c(b)(a)})}]);return c},e=/^NG_ENABLE_DEBUG_INFO!/,f=/^NG_DEFER_BOOTSTRAP!/;C&&e.test(C.name)&&(d.debugInfoEnabled=!0,C.name=C.name.replace(e,""));if(C&&!f.test(C.name))return c();C.name=C.name.replace(f,"");ca.resumeBootstrap=function(a){r(a,function(a){b.push(a)});return c()};B(ca.resumeDeferredBootstrap)&&
+ca.resumeDeferredBootstrap()}function Ce(){C.name="NG_ENABLE_DEBUG_INFO!"+C.name;C.location.reload()}function De(a){a=ca.element(a).injector();if(!a)throw pa("test");return a.get("$$testability")}function Vc(a,b){b=b||"_";return a.replace(Ee,function(a,c){return(c?b:"")+a.toLowerCase()})}function Fe(){var a;if(!Wc){var b=qb();(rb=z(b)?C.jQuery:b?C[b]:void 0)&&rb.fn.on?(x=rb,S(rb.fn,{scope:Wa.scope,isolateScope:Wa.isolateScope,controller:Wa.controller,injector:Wa.injector,inheritedData:Wa.inheritedData})):
+x=Y;a=x.cleanData;x.cleanData=function(b){for(var c,e=0,f;null!=(f=b[e]);e++)(c=(x._data(f)||{}).events)&&c.$destroy&&x(f).triggerHandler("$destroy");a(b)};ca.element=x;Wc=!0}}function gb(a,b,d){if(!a)throw pa("areq",b||"?",d||"required");return a}function sb(a,b,d){d&&H(a)&&(a=a[a.length-1]);gb(B(a),b,"not a function, got "+(a&&"object"===typeof a?a.constructor.name||"Object":typeof a));return a}function Ja(a,b){if("hasOwnProperty"===a)throw pa("badname",b);}function Ge(a,b,d){if(!b)return a;b=b.split(".");
+for(var c,e=a,f=b.length,g=0;g$2>")+c[2];for(c=c[0];c--;)d=d.lastChild;f=db(f,d.childNodes);d=e.firstChild;d.textContent=""}else f.push(b.createTextNode(a));e.textContent="";e.innerHTML="";r(f,function(a){e.appendChild(a)});return e}function Y(a){if(a instanceof Y)return a;var b;A(a)&&(a=U(a),b=!0);if(!(this instanceof Y)){if(b&&"<"!==a.charAt(0))throw nc("nosel");return new Y(a)}if(b){b=
+C.document;var d;a=(d=og.exec(a))?[b.createElement(d[1])]:(d=ed(a,b))?d.childNodes:[];oc(this,a)}else B(a)?fd(a):oc(this,a)}function pc(a){return a.cloneNode(!0)}function yb(a,b){!b&&lc(a)&&x.cleanData([a]);a.querySelectorAll&&x.cleanData(a.querySelectorAll("*"))}function gd(a){for(var b in a)return!1;return!0}function hd(a){var b=a.ng339,d=b&&Ka[b],c=d&&d.events,d=d&&d.data;d&&!gd(d)||c&&!gd(c)||(delete Ka[b],a.ng339=void 0)}function id(a,b,d,c){if(w(c))throw nc("offargs");var e=(c=zb(a))&&c.events,
+f=c&&c.handle;if(f){if(b){var g=function(b){var c=e[b];w(d)&&cb(c||[],d);w(d)&&c&&0l&&this.remove(n.key);return b}},get:function(a){if(l";b=Fa.firstChild.attributes;var d=b[0];b.removeNamedItem(d.name);d.value=c;a.attributes.setNamedItem(d)}function sa(a,b){try{a.addClass(b)}catch(c){}}
+function da(a,b,c,d,e){a instanceof x||(a=x(a));var f=Xa(a,b,a,c,d,e);da.$$addScopeClass(a);var g=null;return function(b,c,d){if(!a)throw $("multilink");gb(b,"scope");e&&e.needsNewScope&&(b=b.$parent.$new());d=d||{};var h=d.parentBoundTranscludeFn,k=d.transcludeControllers;d=d.futureParentElement;h&&h.$$boundTransclude&&(h=h.$$boundTransclude);g||(g=(d=d&&d[0])?"foreignobject"!==ua(d)&&la.call(d).match(/SVG/)?"svg":"html":"html");d="html"!==g?x(ja(g,x("
").append(a).html())):c?Wa.clone.call(a):
+a;if(k)for(var l in k)d.data("$"+l+"Controller",k[l].instance);da.$$addScopeInfo(d,b);c&&c(d,b);f&&f(b,d,d,h);c||(a=f=null);return d}}function Xa(a,b,c,d,e,f){function g(a,c,d,e){var f,k,l,m,p,I,t;if(n)for(t=Array(c.length),m=0;mu.priority)break;if(O=u.scope)u.templateUrl||(D(O)?(ba("new/isolated scope",s||t,u,y),s=u):ba("new/isolated scope",s,u,y)),t=t||u;Q=u.name;if(!ma&&(u.replace&&(u.templateUrl||u.template)||u.transclude&&!u.$$tlb)){for(O=sa+1;ma=a[O++];)if(ma.transclude&&!ma.$$tlb||ma.replace&&(ma.templateUrl||ma.template)){Ib=!0;break}ma=!0}!u.templateUrl&&u.controller&&(J=J||T(),ba("'"+Q+"' controller",
+J[Q],u,y),J[Q]=u);if(O=u.transclude)if(G=!0,u.$$tlb||(ba("transclusion",L,u,y),L=u),"element"===O)N=!0,n=u.priority,M=y,y=d.$$element=x(da.$$createComment(Q,d[Q])),b=y[0],pa(f,Ha.call(M,0),b),R=Z(Ib,M,e,n,g&&g.name,{nonTlbTranscludeDirective:L});else{var ka=T();if(D(O)){M=C.document.createDocumentFragment();var Xa=T(),F=T();r(O,function(a,b){var c="?"===a.charAt(0);a=c?a.substring(1):a;Xa[a]=b;ka[b]=null;F[b]=c});r(y.contents(),function(a){var b=Xa[wa(ua(a))];b?(F[b]=!0,ka[b]=ka[b]||C.document.createDocumentFragment(),
+ka[b].appendChild(a)):M.appendChild(a)});r(F,function(a,b){if(!a)throw $("reqslot",b);});for(var K in ka)ka[K]&&(R=x(ka[K].childNodes),ka[K]=Z(Ib,R,e));M=x(M.childNodes)}else M=x(pc(b)).contents();y.empty();R=Z(Ib,M,e,void 0,void 0,{needsNewScope:u.$$isolateScope||u.$$newScope});R.$$slots=ka}if(u.template)if(P=!0,ba("template",v,u,y),v=u,O=B(u.template)?u.template(y,d):u.template,O=Na(O),u.replace){g=u;M=mc.test(O)?rd(ja(u.templateNamespace,U(O))):[];b=M[0];if(1!==M.length||1!==b.nodeType)throw $("tplrt",
+Q,"");pa(f,y,b);A={$attr:{}};O=sc(b,[],A);var Dg=a.splice(sa+1,a.length-(sa+1));(s||t)&&fa(O,s,t);a=a.concat(O).concat(Dg);ga(d,A);A=a.length}else y.html(O);if(u.templateUrl)P=!0,ba("template",v,u,y),v=u,u.replace&&(g=u),p=ha(a.splice(sa,a.length-sa),y,d,f,G&&R,h,k,{controllerDirectives:J,newScopeDirective:t!==u&&t,newIsolateScopeDirective:s,templateDirective:v,nonTlbTranscludeDirective:L}),A=a.length;else if(u.compile)try{q=u.compile(y,d,R);var X=u.$$originalDirective||u;B(q)?m(null,Va(X,q),E,ib):
+q&&m(Va(X,q.pre),Va(X,q.post),E,ib)}catch(ca){c(ca,za(y))}u.terminal&&(p.terminal=!0,n=Math.max(n,u.priority))}p.scope=t&&!0===t.scope;p.transcludeOnThisElement=G;p.templateOnThisElement=P;p.transclude=R;l.hasElementTranscludeDirective=N;return p}function W(a,b,c,d){var e;if(A(b)){var f=b.match(l);b=b.substring(f[0].length);var g=f[1]||f[3],f="?"===f[2];"^^"===g?c=c.parent():e=(e=d&&d[b])&&e.instance;if(!e){var h="$"+b+"Controller";e="^^"===g&&c[0]&&9===c[0].nodeType?null:g?c.inheritedData(h):c.data(h)}if(!e&&
+!f)throw $("ctreq",b,a);}else if(H(b))for(e=[],g=0,f=b.length;gc.priority)&&-1!==c.restrict.indexOf(e)){k&&(c=ac(c,{$$start:k,$$end:l}));if(!c.$$bindings){var I=m=c,t=c.name,u={isolateScope:null,bindToController:null};D(I.scope)&&(!0===I.bindToController?(u.bindToController=d(I.scope,t,!0),u.isolateScope={}):u.isolateScope=d(I.scope,t,!1));D(I.bindToController)&&(u.bindToController=d(I.bindToController,
+t,!0));if(u.bindToController&&!I.controller)throw $("noctrl",t);m=m.$$bindings=u;D(m.isolateScope)&&(c.$$isolateBindings=m.isolateScope)}b.push(c);m=c}}return m}function ca(b){if(f.hasOwnProperty(b))for(var c=a.get(b+"Directive"),d=0,e=c.length;d"+b+""+a+">";return c.childNodes[0].childNodes;default:return b}}function oa(a,b){if("srcdoc"===b)return u.HTML;if("src"===b||"ngSrc"===b)return-1===["img","video","audio","source","track"].indexOf(a)?u.RESOURCE_URL:u.MEDIA_URL;if("xlinkHref"===b)return"image"===a?u.MEDIA_URL:
+"a"===a?u.URL:u.RESOURCE_URL;if("form"===a&&"action"===b||"base"===a&&"href"===b||"link"===a&&"href"===b)return u.RESOURCE_URL;if("a"===a&&("href"===b||"ngHref"===b))return u.URL}function xa(a,b){var c=b.toLowerCase();return v[a+"|"+c]||v["*|"+c]}function ya(a){return ma(u.valueOf(a),"ng-prop-srcset")}function Ea(a,b,c,d){if(m.test(d))throw $("nodomevents");a=ua(a);var e=xa(a,d),f=Ta;"srcset"!==d||"img"!==a&&"source"!==a?e&&(f=u.getTrusted.bind(u,e)):f=ya;b.push({priority:100,compile:function(a,b){var e=
+p(b[c]),g=p(b[c],function(a){return u.valueOf(a)});return{pre:function(a,b){function c(){var g=e(a);b[0][d]=f(g)}c();a.$watch(g,c)}}}})}function Ia(a,c,d,e,f){var g=ua(a),k=oa(g,e),l=h[e]||f,p=b(d,!f,k,l);if(p){if("multiple"===e&&"select"===g)throw $("selmulti",za(a));if(m.test(e))throw $("nodomevents");c.push({priority:100,compile:function(){return{pre:function(a,c,f){c=f.$$observers||(f.$$observers=T());var g=f[e];g!==d&&(p=g&&b(g,!0,k,l),d=g);p&&(f[e]=p(a),(c[e]||(c[e]=[])).$$inter=!0,(f.$$observers&&
+f.$$observers[e].$$scope||a).$watch(p,function(a,b){"class"===e&&a!==b?f.$updateClass(a,b):f.$set(e,a)}))}}}})}}function pa(a,b,c){var d=b[0],e=b.length,f=d.parentNode,g,h;if(a)for(g=0,h=a.length;g=b)return a;for(;b--;){var d=a[b];(8===d.nodeType||d.nodeType===Pa&&""===d.nodeValue.trim())&&Fg.call(a,b,1)}return a}function Bg(a,b){if(b&&A(b))return b;if(A(a)){var d=ud.exec(a);if(d)return d[3]}}function Ff(){var a={};this.has=function(b){return a.hasOwnProperty(b)};this.register=function(b,d){Ja(b,
+"controller");D(b)?S(a,b):a[b]=d};this.$get=["$injector",function(b){function d(a,b,d,g){if(!a||!D(a.$scope))throw F("$controller")("noscp",g,b);a.$scope[b]=d}return function(c,e,f,g){var k,h,l;f=!0===f;g&&A(g)&&(l=g);if(A(c)){g=c.match(ud);if(!g)throw vd("ctrlfmt",c);h=g[1];l=l||g[3];c=a.hasOwnProperty(h)?a[h]:Ge(e.$scope,h,!0);if(!c)throw vd("ctrlreg",h);sb(c,h,!0)}if(f)return f=(H(c)?c[c.length-1]:c).prototype,k=Object.create(f||null),l&&d(e,l,k,h||c.name),S(function(){var a=b.invoke(c,k,e,h);
+a!==k&&(D(a)||B(a))&&(k=a,l&&d(e,l,k,h||c.name));return k},{instance:k,identifier:l});k=b.instantiate(c,e,h);l&&d(e,l,k,h||c.name);return k}}]}function Gf(){this.$get=["$window",function(a){return x(a.document)}]}function Hf(){this.$get=["$document","$rootScope",function(a,b){function d(){e=c.hidden}var c=a[0],e=c&&c.hidden;a.on("visibilitychange",d);b.$on("$destroy",function(){a.off("visibilitychange",d)});return function(){return e}}]}function If(){this.$get=["$log",function(a){return function(b,
+d){a.error.apply(a,arguments)}}]}function uc(a){return D(a)?ha(a)?a.toISOString():eb(a):a}function Of(){this.$get=function(){return function(a){if(!a)return"";var b=[];Oc(a,function(a,c){null===a||z(a)||B(a)||(H(a)?r(a,function(a){b.push(ba(c)+"="+ba(uc(a)))}):b.push(ba(c)+"="+ba(uc(a))))});return b.join("&")}}}function Pf(){this.$get=function(){return function(a){function b(a,e,f){H(a)?r(a,function(a,c){b(a,e+"["+(D(a)?c:"")+"]")}):D(a)&&!ha(a)?Oc(a,function(a,c){b(a,e+(f?"":"[")+c+(f?"":"]"))}):
+(B(a)&&(a=a()),d.push(ba(e)+"="+(null==a?"":ba(uc(a)))))}if(!a)return"";var d=[];b(a,"",!0);return d.join("&")}}}function vc(a,b){if(A(a)){var d=a.replace(Gg,"").trim();if(d){var c=b("Content-Type"),c=c&&0===c.indexOf(wd),e;(e=c)||(e=(e=d.match(Hg))&&Ig[e[0]].test(d));if(e)try{a=Rc(d)}catch(f){if(!c)return a;throw Kb("baddata",a,f);}}}return a}function xd(a){var b=T(),d;A(a)?r(a.split("\n"),function(a){d=a.indexOf(":");var e=K(U(a.substr(0,d)));a=U(a.substr(d+1));e&&(b[e]=b[e]?b[e]+", "+a:a)}):D(a)&&
+r(a,function(a,d){var f=K(d),g=U(a);f&&(b[f]=b[f]?b[f]+", "+g:g)});return b}function yd(a){var b;return function(d){b||(b=xd(a));return d?(d=b[K(d)],void 0===d&&(d=null),d):b}}function zd(a,b,d,c){if(B(c))return c(a,b,d);r(c,function(c){a=c(a,b,d)});return a}function Nf(){var a=this.defaults={transformResponse:[vc],transformRequest:[function(a){return D(a)&&"[object File]"!==la.call(a)&&"[object Blob]"!==la.call(a)&&"[object FormData]"!==la.call(a)?eb(a):a}],headers:{common:{Accept:"application/json, text/plain, */*"},
+post:ja(wc),put:ja(wc),patch:ja(wc)},xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",paramSerializer:"$httpParamSerializer",jsonpCallbackParam:"callback"},b=!1;this.useApplyAsync=function(a){return w(a)?(b=!!a,this):b};var d=this.interceptors=[],c=this.xsrfWhitelistedOrigins=[];this.$get=["$browser","$httpBackend","$$cookieReader","$cacheFactory","$rootScope","$q","$injector","$sce",function(e,f,g,k,h,l,m,p){function n(b){function c(a,b){for(var d=0,e=b.length;da?b:l.reject(b)}if(!D(b))throw F("$http")("badreq",b);if(!A(p.valueOf(b.url)))throw F("$http")("badreq",b.url);var g=S({method:"get",transformRequest:a.transformRequest,transformResponse:a.transformResponse,paramSerializer:a.paramSerializer,jsonpCallbackParam:a.jsonpCallbackParam},
+b);g.headers=function(b){var c=a.headers,e=S({},b.headers),f,g,h,c=S({},c.common,c[K(b.method)]);a:for(f in c){g=K(f);for(h in e)if(K(h)===g)continue a;e[f]=c[f]}return d(e,ja(b))}(b);g.method=ub(g.method);g.paramSerializer=A(g.paramSerializer)?m.get(g.paramSerializer):g.paramSerializer;e.$$incOutstandingRequestCount("$http");var h=[],k=[];b=l.resolve(g);r(v,function(a){(a.request||a.requestError)&&h.unshift(a.request,a.requestError);(a.response||a.responseError)&&k.push(a.response,a.responseError)});
+b=c(b,h);b=b.then(function(b){var c=b.headers,d=zd(b.data,yd(c),void 0,b.transformRequest);z(d)&&r(c,function(a,b){"content-type"===K(b)&&delete c[b]});z(b.withCredentials)&&!z(a.withCredentials)&&(b.withCredentials=a.withCredentials);return s(b,d).then(f,f)});b=c(b,k);return b=b.finally(function(){e.$$completeOutstandingRequest(E,"$http")})}function s(c,d){function e(a){if(a){var c={};r(a,function(a,d){c[d]=function(c){function d(){a(c)}b?h.$applyAsync(d):h.$$phase?d():h.$apply(d)}});return c}}function k(a,
+c,d,e,f){function g(){m(c,a,d,e,f)}R&&(200<=a&&300>a?R.put(O,[a,c,xd(d),e,f]):R.remove(O));b?h.$applyAsync(g):(g(),h.$$phase||h.$apply())}function m(a,b,d,e,f){b=-1<=b?b:0;(200<=b&&300>b?L.resolve:L.reject)({data:a,status:b,headers:yd(d),config:c,statusText:e,xhrStatus:f})}function s(a){m(a.data,a.status,ja(a.headers()),a.statusText,a.xhrStatus)}function v(){var a=n.pendingRequests.indexOf(c);-1!==a&&n.pendingRequests.splice(a,1)}var L=l.defer(),u=L.promise,R,q,ma=c.headers,x="jsonp"===K(c.method),
+O=c.url;x?O=p.getTrustedResourceUrl(O):A(O)||(O=p.valueOf(O));O=G(O,c.paramSerializer(c.params));x&&(O=t(O,c.jsonpCallbackParam));n.pendingRequests.push(c);u.then(v,v);!c.cache&&!a.cache||!1===c.cache||"GET"!==c.method&&"JSONP"!==c.method||(R=D(c.cache)?c.cache:D(a.cache)?a.cache:N);R&&(q=R.get(O),w(q)?q&&B(q.then)?q.then(s,s):H(q)?m(q[1],q[0],ja(q[2]),q[3],q[4]):m(q,200,{},"OK","complete"):R.put(O,u));z(q)&&((q=jc(c.url)?g()[c.xsrfCookieName||a.xsrfCookieName]:void 0)&&(ma[c.xsrfHeaderName||a.xsrfHeaderName]=
+q),f(c.method,O,d,k,ma,c.timeout,c.withCredentials,c.responseType,e(c.eventHandlers),e(c.uploadEventHandlers)));return u}function G(a,b){0=h&&(t.resolve(s),f(r.$$intervalId));G||c.$apply()},k,t,G);return r}}}]}function Ad(a,b){var d=ga(a);b.$$protocol=d.protocol;b.$$host=d.hostname;b.$$port=fa(d.port)||Mg[d.protocol]||null}function Bd(a,b,d){if(Ng.test(a))throw jb("badpath",a);var c="/"!==a.charAt(0);c&&(a="/"+a);a=ga(a);for(var c=(c&&"/"===a.pathname.charAt(0)?a.pathname.substring(1):a.pathname).split("/"),e=c.length;e--;)c[e]=decodeURIComponent(c[e]),d&&(c[e]=c[e].replace(/\//g,"%2F"));d=c.join("/");b.$$path=d;b.$$search=gc(a.search);
+b.$$hash=decodeURIComponent(a.hash);b.$$path&&"/"!==b.$$path.charAt(0)&&(b.$$path="/"+b.$$path)}function xc(a,b){return a.slice(0,b.length)===b}function xa(a,b){if(xc(b,a))return b.substr(a.length)}function Da(a){var b=a.indexOf("#");return-1===b?a:a.substr(0,b)}function yc(a,b,d){this.$$html5=!0;d=d||"";Ad(a,this);this.$$parse=function(a){var d=xa(b,a);if(!A(d))throw jb("ipthprfx",a,b);Bd(d,this,!0);this.$$path||(this.$$path="/");this.$$compose()};this.$$normalizeUrl=function(a){return b+a.substr(1)};
+this.$$parseLinkUrl=function(c,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),!0;var f,g;w(f=xa(a,c))?(g=f,g=d&&w(f=xa(d,f))?b+(xa("/",f)||f):a+g):w(f=xa(b,c))?g=b+f:b===c+"/"&&(g=b);g&&this.$$parse(g);return!!g}}function zc(a,b,d){Ad(a,this);this.$$parse=function(c){var e=xa(a,c)||xa(b,c),f;z(e)||"#"!==e.charAt(0)?this.$$html5?f=e:(f="",z(e)&&(a=c,this.replace())):(f=xa(d,e),z(f)&&(f=e));Bd(f,this,!1);c=this.$$path;var e=a,g=/^\/[A-Z]:(\/.*)/;xc(f,e)&&(f=f.replace(e,""));g.exec(f)||(c=(f=g.exec(c))?
+f[1]:c);this.$$path=c;this.$$compose()};this.$$normalizeUrl=function(b){return a+(b?d+b:"")};this.$$parseLinkUrl=function(b,d){return Da(a)===Da(b)?(this.$$parse(b),!0):!1}}function Cd(a,b,d){this.$$html5=!0;zc.apply(this,arguments);this.$$parseLinkUrl=function(c,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),!0;var f,g;a===Da(c)?f=c:(g=xa(b,c))?f=a+d+g:b===c+"/"&&(f=b);f&&this.$$parse(f);return!!f};this.$$normalizeUrl=function(b){return a+d+b}}function Lb(a){return function(){return this[a]}}function Dd(a,
+b){return function(d){if(z(d))return this[a];this[a]=b(d);this.$$compose();return this}}function Tf(){var a="!",b={enabled:!1,requireBase:!0,rewriteLinks:!0};this.hashPrefix=function(b){return w(b)?(a=b,this):a};this.html5Mode=function(a){if(Ga(a))return b.enabled=a,this;if(D(a)){Ga(a.enabled)&&(b.enabled=a.enabled);Ga(a.requireBase)&&(b.requireBase=a.requireBase);if(Ga(a.rewriteLinks)||A(a.rewriteLinks))b.rewriteLinks=a.rewriteLinks;return this}return b};this.$get=["$rootScope","$browser","$sniffer",
+"$rootElement","$window",function(d,c,e,f,g){function k(a,b){return a===b||ga(a).href===ga(b).href}function h(a,b,d){var e=m.url(),f=m.$$state;try{c.url(a,b,d),m.$$state=c.state()}catch(g){throw m.url(e),m.$$state=f,g;}}function l(a,b){d.$broadcast("$locationChangeSuccess",m.absUrl(),a,m.$$state,b)}var m,p;p=c.baseHref();var n=c.url(),s;if(b.enabled){if(!p&&b.requireBase)throw jb("nobase");s=n.substring(0,n.indexOf("/",n.indexOf("//")+2))+(p||"/");p=e.history?yc:Cd}else s=Da(n),p=zc;var r=s.substr(0,
+Da(s).lastIndexOf("/")+1);m=new p(s,r,"#"+a);m.$$parseLinkUrl(n,n);m.$$state=c.state();var t=/^\s*(javascript|mailto):/i;f.on("click",function(a){var e=b.rewriteLinks;if(e&&!a.ctrlKey&&!a.metaKey&&!a.shiftKey&&2!==a.which&&2!==a.button){for(var g=x(a.target);"a"!==ua(g[0]);)if(g[0]===f[0]||!(g=g.parent())[0])return;if(!A(e)||!z(g.attr(e))){var e=g.prop("href"),h=g.attr("href")||g.attr("xlink:href");D(e)&&"[object SVGAnimatedString]"===e.toString()&&(e=ga(e.animVal).href);t.test(e)||!e||g.attr("target")||
+a.isDefaultPrevented()||!m.$$parseLinkUrl(e,h)||(a.preventDefault(),m.absUrl()!==c.url()&&d.$apply())}}});m.absUrl()!==n&&c.url(m.absUrl(),!0);var N=!0;c.onUrlChange(function(a,b){xc(a,r)?(d.$evalAsync(function(){var c=m.absUrl(),e=m.$$state,f;m.$$parse(a);m.$$state=b;f=d.$broadcast("$locationChangeStart",a,c,b,e).defaultPrevented;m.absUrl()===a&&(f?(m.$$parse(c),m.$$state=e,h(c,!1,e)):(N=!1,l(c,e)))}),d.$$phase||d.$digest()):g.location.href=a});d.$watch(function(){if(N||m.$$urlUpdatedByLocation){m.$$urlUpdatedByLocation=
+!1;var a=c.url(),b=m.absUrl(),f=c.state(),g=m.$$replace,n=!k(a,b)||m.$$html5&&e.history&&f!==m.$$state;if(N||n)N=!1,d.$evalAsync(function(){var b=m.absUrl(),c=d.$broadcast("$locationChangeStart",b,a,m.$$state,f).defaultPrevented;m.absUrl()===b&&(c?(m.$$parse(a),m.$$state=f):(n&&h(b,g,f===m.$$state?null:m.$$state),l(a,f)))})}m.$$replace=!1});return m}]}function Uf(){var a=!0,b=this;this.debugEnabled=function(b){return w(b)?(a=b,this):a};this.$get=["$window",function(d){function c(a){cc(a)&&(a.stack&&
+f?a=a.message&&-1===a.stack.indexOf(a.message)?"Error: "+a.message+"\n"+a.stack:a.stack:a.sourceURL&&(a=a.message+"\n"+a.sourceURL+":"+a.line));return a}function e(a){var b=d.console||{},e=b[a]||b.log||E;return function(){var a=[];r(arguments,function(b){a.push(c(b))});return Function.prototype.apply.call(e,b,a)}}var f=Ca||/\bEdge\//.test(d.navigator&&d.navigator.userAgent);return{log:e("log"),info:e("info"),warn:e("warn"),error:e("error"),debug:function(){var c=e("debug");return function(){a&&c.apply(b,
+arguments)}}()}}]}function Og(a){return a+""}function Pg(a,b){return"undefined"!==typeof a?a:b}function Ed(a,b){return"undefined"===typeof a?b:"undefined"===typeof b?a:a+b}function Qg(a,b){switch(a.type){case q.MemberExpression:if(a.computed)return!1;break;case q.UnaryExpression:return 1;case q.BinaryExpression:return"+"!==a.operator?1:!1;case q.CallExpression:return!1}return void 0===b?Fd:b}function Z(a,b,d){var c,e,f=a.isPure=Qg(a,d);switch(a.type){case q.Program:c=!0;r(a.body,function(a){Z(a.expression,
+b,f);c=c&&a.expression.constant});a.constant=c;break;case q.Literal:a.constant=!0;a.toWatch=[];break;case q.UnaryExpression:Z(a.argument,b,f);a.constant=a.argument.constant;a.toWatch=a.argument.toWatch;break;case q.BinaryExpression:Z(a.left,b,f);Z(a.right,b,f);a.constant=a.left.constant&&a.right.constant;a.toWatch=a.left.toWatch.concat(a.right.toWatch);break;case q.LogicalExpression:Z(a.left,b,f);Z(a.right,b,f);a.constant=a.left.constant&&a.right.constant;a.toWatch=a.constant?[]:[a];break;case q.ConditionalExpression:Z(a.test,
+b,f);Z(a.alternate,b,f);Z(a.consequent,b,f);a.constant=a.test.constant&&a.alternate.constant&&a.consequent.constant;a.toWatch=a.constant?[]:[a];break;case q.Identifier:a.constant=!1;a.toWatch=[a];break;case q.MemberExpression:Z(a.object,b,f);a.computed&&Z(a.property,b,f);a.constant=a.object.constant&&(!a.computed||a.property.constant);a.toWatch=a.constant?[]:[a];break;case q.CallExpression:c=d=a.filter?!b(a.callee.name).$stateful:!1;e=[];r(a.arguments,function(a){Z(a,b,f);c=c&&a.constant;e.push.apply(e,
+a.toWatch)});a.constant=c;a.toWatch=d?e:[a];break;case q.AssignmentExpression:Z(a.left,b,f);Z(a.right,b,f);a.constant=a.left.constant&&a.right.constant;a.toWatch=[a];break;case q.ArrayExpression:c=!0;e=[];r(a.elements,function(a){Z(a,b,f);c=c&&a.constant;e.push.apply(e,a.toWatch)});a.constant=c;a.toWatch=e;break;case q.ObjectExpression:c=!0;e=[];r(a.properties,function(a){Z(a.value,b,f);c=c&&a.value.constant;e.push.apply(e,a.value.toWatch);a.computed&&(Z(a.key,b,!1),c=c&&a.key.constant,e.push.apply(e,
+a.key.toWatch))});a.constant=c;a.toWatch=e;break;case q.ThisExpression:a.constant=!1;a.toWatch=[];break;case q.LocalsExpression:a.constant=!1,a.toWatch=[]}}function Gd(a){if(1===a.length){a=a[0].expression;var b=a.toWatch;return 1!==b.length?b:b[0]!==a?b:void 0}}function Hd(a){return a.type===q.Identifier||a.type===q.MemberExpression}function Id(a){if(1===a.body.length&&Hd(a.body[0].expression))return{type:q.AssignmentExpression,left:a.body[0].expression,right:{type:q.NGValueParameter},operator:"="}}
+function Jd(a){this.$filter=a}function Kd(a){this.$filter=a}function Mb(a,b,d){this.ast=new q(a,d);this.astCompiler=d.csp?new Kd(b):new Jd(b)}function Ac(a){return B(a.valueOf)?a.valueOf():Rg.call(a)}function Vf(){var a=T(),b={"true":!0,"false":!1,"null":null,undefined:void 0},d,c;this.addLiteral=function(a,c){b[a]=c};this.setIdentifierFns=function(a,b){d=a;c=b;return this};this.$get=["$filter",function(e){function f(b,c){var d,f;switch(typeof b){case "string":return f=b=b.trim(),d=a[f],d||(d=new Nb(G),
+d=(new Mb(d,e,G)).parse(b),a[f]=p(d)),s(d,c);case "function":return s(b,c);default:return s(E,c)}}function g(a,b,c){return null==a||null==b?a===b:"object"!==typeof a||(a=Ac(a),"object"!==typeof a||c)?a===b||a!==a&&b!==b:!1}function k(a,b,c,d,e){var f=d.inputs,h;if(1===f.length){var k=g,f=f[0];return a.$watch(function(a){var b=f(a);g(b,k,f.isPure)||(h=d(a,void 0,void 0,[b]),k=b&&Ac(b));return h},b,c,e)}for(var l=[],m=[],n=0,p=f.length;n=c.$$state.status&&e&&e.length&&a(function(){for(var a,c,f=0,g=e.length;fa)for(b in l++,f)ta.call(e,b)||(t--,delete f[b])}else f!==e&&(f=e,l++);return l}}c.$$pure=g(a).literal;c.$stateful=!c.$$pure;var d=this,e,f,h,k=1r&&(z=4-r,N[z]||
+(N[z]=[]),N[z].push({msg:B(a.exp)?"fn: "+(a.exp.name||a.exp.toString()):a.exp,newVal:g,oldVal:h}));else if(a===c){s=!1;break a}}catch(E){f(E)}if(!(n=!q.$$suspended&&q.$$watchersCount&&q.$$childHead||q!==y&&q.$$nextSibling))for(;q!==y&&!(n=q.$$nextSibling);)q=q.$parent}while(q=n);if((s||w.length)&&!r--)throw v.$$phase=null,d("infdig",b,N);}while(s||w.length);for(v.$$phase=null;JCa)throw Ea("iequirks");var c=ja(V);c.isEnabled=function(){return a};
+c.trustAs=d.trustAs;c.getTrusted=d.getTrusted;c.valueOf=d.valueOf;a||(c.trustAs=c.getTrusted=function(a,b){return b},c.valueOf=Ta);c.parseAs=function(a,d){var e=b(d);return e.literal&&e.constant?e:b(d,function(b){return c.getTrusted(a,b)})};var e=c.parseAs,f=c.getTrusted,g=c.trustAs;r(V,function(a,b){var d=K(b);c[("parse_as_"+d).replace(Cc,wb)]=function(b){return e(a,b)};c[("get_trusted_"+d).replace(Cc,wb)]=function(b){return f(a,b)};c[("trust_as_"+d).replace(Cc,wb)]=function(b){return g(a,b)}});
+return c}]}function ag(){this.$get=["$window","$document",function(a,b){var d={},c=!((!a.nw||!a.nw.process)&&a.chrome&&(a.chrome.app&&a.chrome.app.runtime||!a.chrome.app&&a.chrome.runtime&&a.chrome.runtime.id))&&a.history&&a.history.pushState,e=fa((/android (\d+)/.exec(K((a.navigator||{}).userAgent))||[])[1]),f=/Boxee/i.test((a.navigator||{}).userAgent),g=b[0]||{},k=g.body&&g.body.style,h=!1,l=!1;k&&(h=!!("transition"in k||"webkitTransition"in k),l=!!("animation"in k||"webkitAnimation"in k));return{history:!(!c||
+4>e||f),hasEvent:function(a){if("input"===a&&Ca)return!1;if(z(d[a])){var b=g.createElement("div");d[a]="on"+a in b}return d[a]},csp:Aa(),transitions:h,animations:l,android:e}}]}function bg(){this.$get=ia(function(a){return new Tg(a)})}function Tg(a){function b(){var a=e.pop();return a&&a.cb}function d(a){for(var b=e.length-1;0<=b;--b){var c=e[b];if(c.type===a)return e.splice(b,1),c.cb}}var c={},e=[],f=this.ALL_TASKS_TYPE="$$all$$",g=this.DEFAULT_TASK_TYPE="$$default$$";this.completeTask=function(e,
+h){h=h||g;try{e()}finally{var l;l=h||g;c[l]&&(c[l]--,c[f]--);l=c[h];var m=c[f];if(!m||!l)for(l=m?d:b;m=l(h);)try{m()}catch(p){a.error(p)}}};this.incTaskCount=function(a){a=a||g;c[a]=(c[a]||0)+1;c[f]=(c[f]||0)+1};this.notifyWhenNoPendingTasks=function(a,b){b=b||f;c[b]?e.push({type:b,cb:a}):a()}}function dg(){var a;this.httpOptions=function(b){return b?(a=b,this):a};this.$get=["$exceptionHandler","$templateCache","$http","$q","$sce",function(b,d,c,e,f){function g(k,h){g.totalPendingRequests++;if(!A(k)||
+z(d.get(k)))k=f.getTrustedResourceUrl(k);var l=c.defaults&&c.defaults.transformResponse;H(l)?l=l.filter(function(a){return a!==vc}):l===vc&&(l=null);return c.get(k,S({cache:d,transformResponse:l},a)).finally(function(){g.totalPendingRequests--}).then(function(a){return d.put(k,a.data)},function(a){h||(a=Ug("tpload",k,a.status,a.statusText),b(a));return e.reject(a)})}g.totalPendingRequests=0;return g}]}function eg(){this.$get=["$rootScope","$browser","$location",function(a,b,d){return{findBindings:function(a,
+b,d){a=a.getElementsByClassName("ng-binding");var g=[];r(a,function(a){var c=ca.element(a).data("$binding");c&&r(c,function(c){d?(new RegExp("(^|\\s)"+Md(b)+"(\\s|\\||$)")).test(c)&&g.push(a):-1!==c.indexOf(b)&&g.push(a)})});return g},findModels:function(a,b,d){for(var g=["ng-","data-ng-","ng\\:"],k=0;kc&&(c=e),c+=+a.slice(e+1),a=a.substring(0,e)):0>c&&(c=a.length);for(e=0;a.charAt(e)===Ec;e++);if(e===(g=a.length))d=[0],c=1;else{for(g--;a.charAt(g)===Ec;)g--;c-=e;d=[];for(f=0;e<=g;e++,f++)d[f]=+a.charAt(e)}c>Wd&&(d=d.splice(0,Wd-1),b=c-1,c=1);return{d:d,e:b,i:c}}function dh(a,b,d,c){var e=a.d,f=e.length-a.i;b=z(b)?Math.min(Math.max(d,f),c):+b;d=b+a.i;c=e[d];if(0d-1){for(c=0;c>d;c--)e.unshift(0),a.i++;e.unshift(1);a.i++}else e[d-1]++;for(;fk;)h.unshift(0),k++;0=b.lgSize&&k.unshift(h.splice(-b.lgSize,h.length).join(""));h.length>b.gSize;)k.unshift(h.splice(-b.gSize,h.length).join(""));h.length&&k.unshift(h.join(""));h=k.join(d);f.length&&(h+=c+f.join(""));e&&(h+="e+"+e)}return 0>a&&!g?b.negPre+h+b.negSuf:b.posPre+h+b.posSuf}function Ob(a,b,d,c){var e="";if(0>a||c&&0>=a)c?a=-a+1:(a=-a,e="-");for(a=""+a;a.length-d)f+=d;0===f&&-12===d&&(f=12);return Ob(f,b,c,e)}}function kb(a,b,d){return function(c,e){var f=c["get"+a](),g=ub((d?"STANDALONE":"")+(b?"SHORT":"")+a);return e[g][f]}}function Xd(a){var b=(new Date(a,0,1)).getDay();return new Date(a,0,(4>=b?5:12)-b)}function Yd(a){return function(b){var d=Xd(b.getFullYear());b=+new Date(b.getFullYear(),b.getMonth(),b.getDate()+(4-b.getDay()))-+d;b=1+Math.round(b/6048E5);return Ob(b,a)}}function Fc(a,b){return 0>=
+a.getFullYear()?b.ERAS[0]:b.ERAS[1]}function Rd(a){function b(a){var b;if(b=a.match(d)){a=new Date(0);var f=0,g=0,k=b[8]?a.setUTCFullYear:a.setFullYear,h=b[8]?a.setUTCHours:a.setHours;b[9]&&(f=fa(b[9]+b[10]),g=fa(b[9]+b[11]));k.call(a,fa(b[1]),fa(b[2])-1,fa(b[3]));f=fa(b[4]||0)-f;g=fa(b[5]||0)-g;k=fa(b[6]||0);b=Math.round(1E3*parseFloat("0."+(b[7]||0)));h.call(a,f,g,k,b)}return a}var d=/^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/;return function(c,
+d,f){var g="",k=[],h,l;d=d||"mediumDate";d=a.DATETIME_FORMATS[d]||d;A(c)&&(c=eh.test(c)?fa(c):b(c));W(c)&&(c=new Date(c));if(!ha(c)||!isFinite(c.getTime()))return c;for(;d;)(l=fh.exec(d))?(k=db(k,l,1),d=k.pop()):(k.push(d),d=null);var m=c.getTimezoneOffset();f&&(m=ec(f,m),c=fc(c,f,!0));r(k,function(b){h=gh[b];g+=h?h(c,a.DATETIME_FORMATS,m):"''"===b?"'":b.replace(/(^'|'$)/g,"").replace(/''/g,"'")});return g}}function Yg(){return function(a,b){z(b)&&(b=2);return eb(a,b)}}function Zg(){return function(a,
+b,d){b=Infinity===Math.abs(Number(b))?Number(b):fa(b);if(X(b))return a;W(a)&&(a=a.toString());if(!ya(a))return a;d=!d||isNaN(d)?0:fa(d);d=0>d?Math.max(0,a.length+d):d;return 0<=b?Gc(a,d,d+b):0===d?Gc(a,b,a.length):Gc(a,Math.max(0,d+b),d)}}function Gc(a,b,d){return A(a)?a.slice(b,d):Ha.call(a,b,d)}function Td(a){function b(b){return b.map(function(b){var c=1,d=Ta;if(B(b))d=b;else if(A(b)){if("+"===b.charAt(0)||"-"===b.charAt(0))c="-"===b.charAt(0)?-1:1,b=b.substring(1);if(""!==b&&(d=a(b),d.constant))var e=
+d(),d=function(a){return a[e]}}return{get:d,descending:c}})}function d(a){switch(typeof a){case "number":case "boolean":case "string":return!0;default:return!1}}function c(a,b){var c=0,d=a.type,h=b.type;if(d===h){var h=a.value,l=b.value;"string"===d?(h=h.toLowerCase(),l=l.toLowerCase()):"object"===d&&(D(h)&&(h=a.index),D(l)&&(l=b.index));h!==l&&(c=hb||37<=b&&40>=b||m(a,this,this.value)});if(e.hasEvent("paste"))b.on("paste cut drop",m)}b.on("change",l);if(ce[g]&&c.$$hasNativeValidators&&g===d.type)b.on("keydown wheel mousedown",function(a){if(!h){var b=this.validity,
+c=b.badInput,d=b.typeMismatch;h=f.defer(function(){h=null;b.badInput===c&&b.typeMismatch===d||l(a)})}});c.$render=function(){var a=c.$isEmpty(c.$viewValue)?"":c.$viewValue;b.val()!==a&&b.val(a)}}function Qb(a,b){return function(d,c){var e,f;if(ha(d))return d;if(A(d)){'"'===d.charAt(0)&&'"'===d.charAt(d.length-1)&&(d=d.substring(1,d.length-1));if(hh.test(d))return new Date(d);a.lastIndex=0;if(e=a.exec(d))return e.shift(),f=c?{yyyy:c.getFullYear(),MM:c.getMonth()+1,dd:c.getDate(),HH:c.getHours(),mm:c.getMinutes(),
+ss:c.getSeconds(),sss:c.getMilliseconds()/1E3}:{yyyy:1970,MM:1,dd:1,HH:0,mm:0,ss:0,sss:0},r(e,function(a,c){cf.yyyy&&e.setFullYear(f.yyyy),e}return NaN}}function nb(a,b,d,c){return function(e,f,g,k,h,l,m,p){function n(a){return a&&!(a.getTime&&a.getTime()!==a.getTime())}function s(a){return w(a)&&!ha(a)?r(a)||void 0:a}function r(a,b){var c=k.$options.getOption("timezone");v&&v!==c&&(b=Sc(b,ec(v)));var e=d(a,
+b);!isNaN(e)&&c&&(e=fc(e,c));return e}Ic(e,f,g,k,a);Sa(e,f,g,k,h,l);var t="time"===a||"datetimelocal"===a,q,v;k.$parsers.push(function(c){if(k.$isEmpty(c))return null;if(b.test(c))return r(c,q);k.$$parserName=a});k.$formatters.push(function(a){if(a&&!ha(a))throw ob("datefmt",a);if(n(a)){q=a;var b=k.$options.getOption("timezone");b&&(v=b,q=fc(q,b,!0));var d=c;t&&A(k.$options.getOption("timeSecondsFormat"))&&(d=c.replace("ss.sss",k.$options.getOption("timeSecondsFormat")).replace(/:$/,""));a=m("date")(a,
+d,b);t&&k.$options.getOption("timeStripZeroSeconds")&&(a=a.replace(/(?::00)?(?:\.000)?$/,""));return a}v=q=null;return""});if(w(g.min)||g.ngMin){var x=g.min||p(g.ngMin)(e),B=s(x);k.$validators.min=function(a){return!n(a)||z(B)||d(a)>=B};g.$observe("min",function(a){a!==x&&(B=s(a),x=a,k.$validate())})}if(w(g.max)||g.ngMax){var y=g.max||p(g.ngMax)(e),J=s(y);k.$validators.max=function(a){return!n(a)||z(J)||d(a)<=J};g.$observe("max",function(a){a!==y&&(J=s(a),y=a,k.$validate())})}}}function Ic(a,b,d,
+c,e){(c.$$hasNativeValidators=D(b[0].validity))&&c.$parsers.push(function(a){var d=b.prop("validity")||{};if(d.badInput||d.typeMismatch)c.$$parserName=e;else return a})}function de(a){a.$parsers.push(function(b){if(a.$isEmpty(b))return null;if(ih.test(b))return parseFloat(b);a.$$parserName="number"});a.$formatters.push(function(b){if(!a.$isEmpty(b)){if(!W(b))throw ob("numfmt",b);b=b.toString()}return b})}function na(a){w(a)&&!W(a)&&(a=parseFloat(a));return X(a)?void 0:a}function Jc(a){var b=a.toString(),
+d=b.indexOf(".");return-1===d?-1a&&(a=/e-(\d+)$/.exec(b))?Number(a[1]):0:b.length-d-1}function ee(a,b,d){a=Number(a);var c=(a|0)!==a,e=(b|0)!==b,f=(d|0)!==d;if(c||e||f){var g=c?Jc(a):0,k=e?Jc(b):0,h=f?Jc(d):0,g=Math.max(g,k,h),g=Math.pow(10,g);a*=g;b*=g;d*=g;c&&(a=Math.round(a));e&&(b=Math.round(b));f&&(d=Math.round(d))}return 0===(a-b)%d}function fe(a,b,d,c,e){if(w(c)){a=a(c);if(!a.constant)throw ob("constexpr",d,c);return a(b)}return e}function Kc(a,b){function d(a,b){if(!a||!a.length)return[];
+if(!b||!b.length)return a;var c=[],d=0;a:for(;d (?:<\/\1>|)$/,mc=/<|?\w+;/,mg=/<([\w:-]+)/,ng=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,oa={option:[1,''," "],thead:[1,""],col:[2,""],tr:[2,""],td:[3,""],_default:[0,"",""]};oa.optgroup=oa.option;oa.tbody=oa.tfoot=oa.colgroup=oa.caption=oa.thead;oa.th=oa.td;var ug=C.Node.prototype.contains||function(a){return!!(this.compareDocumentPosition(a)&16)},Wa=Y.prototype={ready:fd,toString:function(){var a=[];r(this,function(b){a.push(""+b)});return"["+a.join(", ")+"]"},eq:function(a){return 0<=a?x(this[a]):x(this[this.length+a])},length:0,push:kh,sort:[].sort,splice:[].splice},Gb={};r("multiple selected checked disabled readOnly required open".split(" "),
+function(a){Gb[K(a)]=a});var md={};r("input select option textarea button form details".split(" "),function(a){md[a]=!0});var td={ngMinlength:"minlength",ngMaxlength:"maxlength",ngMin:"min",ngMax:"max",ngPattern:"pattern",ngStep:"step"};r({data:rc,removeData:qc,hasData:function(a){for(var b in Ka[a.ng339])return!0;return!1},cleanData:function(a){for(var b=0,d=a.length;b/,xg=/^[^(]*\(\s*([^)]*)\)/m,nh=/,/,oh=/^\s*(_?)(\S+?)\1\s*$/,vg=/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg,Ba=F("$injector");
+fb.$$annotate=function(a,b,d){var c;if("function"===typeof a){if(!(c=a.$inject)){c=[];if(a.length){if(b)throw A(d)&&d||(d=a.name||yg(a)),Ba("strictdi",d);b=od(a);r(b[1].split(nh),function(a){a.replace(oh,function(a,b,d){c.push(d)})})}a.$inject=c}}else H(a)?(b=a.length-1,sb(a[b],"fn"),c=a.slice(0,b)):sb(a,"fn",!0);return c};var je=F("$animate"),zf=function(){this.$get=E},Af=function(){var a=new Hb,b=[];this.$get=["$$AnimateRunner","$rootScope",function(d,c){function e(a,b,c){var d=!1;b&&(b=A(b)?b.split(" "):
+H(b)?b:[],r(b,function(b){b&&(d=!0,a[b]=c)}));return d}function f(){r(b,function(b){var c=a.get(b);if(c){var d=zg(b.attr("class")),e="",f="";r(c,function(a,b){a!==!!d[b]&&(a?e+=(e.length?" ":"")+b:f+=(f.length?" ":"")+b)});r(b,function(a){e&&Db(a,e);f&&Cb(a,f)});a.delete(b)}});b.length=0}return{enabled:E,on:E,off:E,pin:E,push:function(g,k,h,l){l&&l();h=h||{};h.from&&g.css(h.from);h.to&&g.css(h.to);if(h.addClass||h.removeClass)if(k=h.addClass,l=h.removeClass,h=a.get(g)||{},k=e(h,k,!0),l=e(h,l,!1),
+k||l)a.set(g,h),b.push(g),1===b.length&&c.$$postDigest(f);g=new d;g.complete();return g}}}]},xf=["$provide",function(a){var b=this,d=null,c=null;this.$$registeredAnimations=Object.create(null);this.register=function(c,d){if(c&&"."!==c.charAt(0))throw je("notcsel",c);var g=c+"-animation";b.$$registeredAnimations[c.substr(1)]=g;a.factory(g,d)};this.customFilter=function(a){1===arguments.length&&(c=B(a)?a:null);return c};this.classNameFilter=function(a){if(1===arguments.length&&(d=a instanceof RegExp?
+a:null)&&/[(\s|\/)]ng-animate[(\s|\/)]/.test(d.toString()))throw d=null,je("nongcls","ng-animate");return d};this.$get=["$$animateQueue",function(a){function b(a,c,d){if(d){var e;a:{for(e=0;e <= >= && || ! = |".split(" "),function(a){Ub[a]=!0});var rh={n:"\n",f:"\f",r:"\r",t:"\t",v:"\v","'":"'",'"':'"'},Nb=function(a){this.options=a};Nb.prototype={constructor:Nb,
+lex:function(a){this.text=a;this.index=0;for(this.tokens=[];this.index=a&&"string"===typeof a},isWhitespace:function(a){return" "===a||"\r"===a||"\t"===a||"\n"===a||"\v"===a||"\u00a0"===a},isIdentifierStart:function(a){return this.options.isIdentifierStart?
+this.options.isIdentifierStart(a,this.codePointAt(a)):this.isValidIdentifierStart(a)},isValidIdentifierStart:function(a){return"a"<=a&&"z">=a||"A"<=a&&"Z">=a||"_"===a||"$"===a},isIdentifierContinue:function(a){return this.options.isIdentifierContinue?this.options.isIdentifierContinue(a,this.codePointAt(a)):this.isValidIdentifierContinue(a)},isValidIdentifierContinue:function(a,b){return this.isValidIdentifierStart(a,b)||this.isNumber(a)},codePointAt:function(a){return 1===a.length?a.charCodeAt(0):
+(a.charCodeAt(0)<<10)+a.charCodeAt(1)-56613888},peekMultichar:function(){var a=this.text.charAt(this.index),b=this.peek();if(!b)return a;var d=a.charCodeAt(0),c=b.charCodeAt(0);return 55296<=d&&56319>=d&&56320<=c&&57343>=c?a+b:a},isExpOperator:function(a){return"-"===a||"+"===a||this.isNumber(a)},throwError:function(a,b,d){d=d||this.index;b=w(b)?"s "+b+"-"+this.index+" ["+this.text.substring(b,d)+"]":" "+d;throw Ya("lexerr",a,b,this.text);},readNumber:function(){for(var a="",b=this.index;this.index<
+this.text.length;){var d=K(this.text.charAt(this.index));if("."===d||this.isNumber(d))a+=d;else{var c=this.peek();if("e"===d&&this.isExpOperator(c))a+=d;else if(this.isExpOperator(d)&&c&&this.isNumber(c)&&"e"===a.charAt(a.length-1))a+=d;else if(!this.isExpOperator(d)||c&&this.isNumber(c)||"e"!==a.charAt(a.length-1))break;else this.throwError("Invalid exponent")}this.index++}this.tokens.push({index:b,text:a,constant:!0,value:Number(a)})},readIdent:function(){var a=this.index;for(this.index+=this.peekMultichar().length;this.index<
+this.text.length;){var b=this.peekMultichar();if(!this.isIdentifierContinue(b))break;this.index+=b.length}this.tokens.push({index:a,text:this.text.slice(a,this.index),identifier:!0})},readString:function(a){var b=this.index;this.index++;for(var d="",c=a,e=!1;this.index","<=",">=");)a={type:q.BinaryExpression,operator:b.text,left:a,right:this.additive()};return a},additive:function(){for(var a=this.multiplicative(),b;b=this.expect("+","-");)a={type:q.BinaryExpression,
+operator:b.text,left:a,right:this.multiplicative()};return a},multiplicative:function(){for(var a=this.unary(),b;b=this.expect("*","/","%");)a={type:q.BinaryExpression,operator:b.text,left:a,right:this.unary()};return a},unary:function(){var a;return(a=this.expect("+","-","!"))?{type:q.UnaryExpression,operator:a.text,prefix:!0,argument:this.unary()}:this.primary()},primary:function(){var a;this.expect("(")?(a=this.filterChain(),this.consume(")")):this.expect("[")?a=this.arrayDeclaration():this.expect("{")?
+a=this.object():this.selfReferential.hasOwnProperty(this.peek().text)?a=Ia(this.selfReferential[this.consume().text]):this.options.literals.hasOwnProperty(this.peek().text)?a={type:q.Literal,value:this.options.literals[this.consume().text]}:this.peek().identifier?a=this.identifier():this.peek().constant?a=this.constant():this.throwError("not a primary expression",this.peek());for(var b;b=this.expect("(","[",".");)"("===b.text?(a={type:q.CallExpression,callee:a,arguments:this.parseArguments()},this.consume(")")):
+"["===b.text?(a={type:q.MemberExpression,object:a,property:this.expression(),computed:!0},this.consume("]")):"."===b.text?a={type:q.MemberExpression,object:a,property:this.identifier(),computed:!1}:this.throwError("IMPOSSIBLE");return a},filter:function(a){a=[a];for(var b={type:q.CallExpression,callee:this.identifier(),arguments:a,filter:!0};this.expect(":");)a.push(this.expression());return b},parseArguments:function(){var a=[];if(")"!==this.peekToken().text){do a.push(this.filterChain());while(this.expect(","))
+}return a},identifier:function(){var a=this.consume();a.identifier||this.throwError("is not a valid identifier",a);return{type:q.Identifier,name:a.text}},constant:function(){return{type:q.Literal,value:this.consume().value}},arrayDeclaration:function(){var a=[];if("]"!==this.peekToken().text){do{if(this.peek("]"))break;a.push(this.expression())}while(this.expect(","))}this.consume("]");return{type:q.ArrayExpression,elements:a}},object:function(){var a=[],b;if("}"!==this.peekToken().text){do{if(this.peek("}"))break;
+b={type:q.Property,kind:"init"};this.peek().constant?(b.key=this.constant(),b.computed=!1,this.consume(":"),b.value=this.expression()):this.peek().identifier?(b.key=this.identifier(),b.computed=!1,this.peek(":")?(this.consume(":"),b.value=this.expression()):b.value=b.key):this.peek("[")?(this.consume("["),b.key=this.expression(),this.consume("]"),b.computed=!0,this.consume(":"),b.value=this.expression()):this.throwError("invalid key",this.peek());a.push(b)}while(this.expect(","))}this.consume("}");
+return{type:q.ObjectExpression,properties:a}},throwError:function(a,b){throw Ya("syntax",b.text,a,b.index+1,this.text,this.text.substring(b.index));},consume:function(a){if(0===this.tokens.length)throw Ya("ueoe",this.text);var b=this.expect(a);b||this.throwError("is unexpected, expecting ["+a+"]",this.peek());return b},peekToken:function(){if(0===this.tokens.length)throw Ya("ueoe",this.text);return this.tokens[0]},peek:function(a,b,d,c){return this.peekAhead(0,a,b,d,c)},peekAhead:function(a,b,d,c,
+e){if(this.tokens.length>a){a=this.tokens[a];var f=a.text;if(f===b||f===d||f===c||f===e||!(b||d||c||e))return a}return!1},expect:function(a,b,d,c){return(a=this.peek(a,b,d,c))?(this.tokens.shift(),a):!1},selfReferential:{"this":{type:q.ThisExpression},$locals:{type:q.LocalsExpression}}};var Fd=2;Jd.prototype={compile:function(a){var b=this;this.state={nextId:0,filters:{},fn:{vars:[],body:[],own:{}},assign:{vars:[],body:[],own:{}},inputs:[]};Z(a,b.$filter);var d="",c;this.stage="assign";if(c=Id(a))this.state.computing=
+"assign",d=this.nextId(),this.recurse(c,d),this.return_(d),d="fn.assign="+this.generateFunction("assign","s,v,l");c=Gd(a.body);b.stage="inputs";r(c,function(a,c){var d="fn"+c;b.state[d]={vars:[],body:[],own:{}};b.state.computing=d;var k=b.nextId();b.recurse(a,k);b.return_(k);b.state.inputs.push({name:d,isPure:a.isPure});a.watchId=c});this.state.computing="fn";this.stage="main";this.recurse(a);a='"'+this.USE+" "+this.STRICT+'";\n'+this.filterPrefix()+"var fn="+this.generateFunction("fn","s,l,a,i")+
+d+this.watchFns()+"return fn;";a=(new Function("$filter","getStringValue","ifDefined","plus",a))(this.$filter,Og,Pg,Ed);this.state=this.stage=void 0;return a},USE:"use",STRICT:"strict",watchFns:function(){var a=[],b=this.state.inputs,d=this;r(b,function(b){a.push("var "+b.name+"="+d.generateFunction(b.name,"s"));b.isPure&&a.push(b.name,".isPure="+JSON.stringify(b.isPure)+";")});b.length&&a.push("fn.inputs=["+b.map(function(a){return a.name}).join(",")+"];");return a.join("")},generateFunction:function(a,
+b){return"function("+b+"){"+this.varsPrefix(a)+this.body(a)+"};"},filterPrefix:function(){var a=[],b=this;r(this.state.filters,function(d,c){a.push(d+"=$filter("+b.escape(c)+")")});return a.length?"var "+a.join(",")+";":""},varsPrefix:function(a){return this.state[a].vars.length?"var "+this.state[a].vars.join(",")+";":""},body:function(a){return this.state[a].body.join("")},recurse:function(a,b,d,c,e,f){var g,k,h=this,l,m,p;c=c||E;if(!f&&w(a.watchId))b=b||this.nextId(),this.if_("i",this.lazyAssign(b,
+this.computedMember("i",a.watchId)),this.lazyRecurse(a,b,d,c,e,!0));else switch(a.type){case q.Program:r(a.body,function(b,c){h.recurse(b.expression,void 0,void 0,function(a){k=a});c!==a.body.length-1?h.current().body.push(k,";"):h.return_(k)});break;case q.Literal:m=this.escape(a.value);this.assign(b,m);c(b||m);break;case q.UnaryExpression:this.recurse(a.argument,void 0,void 0,function(a){k=a});m=a.operator+"("+this.ifDefined(k,0)+")";this.assign(b,m);c(m);break;case q.BinaryExpression:this.recurse(a.left,
+void 0,void 0,function(a){g=a});this.recurse(a.right,void 0,void 0,function(a){k=a});m="+"===a.operator?this.plus(g,k):"-"===a.operator?this.ifDefined(g,0)+a.operator+this.ifDefined(k,0):"("+g+")"+a.operator+"("+k+")";this.assign(b,m);c(m);break;case q.LogicalExpression:b=b||this.nextId();h.recurse(a.left,b);h.if_("&&"===a.operator?b:h.not(b),h.lazyRecurse(a.right,b));c(b);break;case q.ConditionalExpression:b=b||this.nextId();h.recurse(a.test,b);h.if_(b,h.lazyRecurse(a.alternate,b),h.lazyRecurse(a.consequent,
+b));c(b);break;case q.Identifier:b=b||this.nextId();d&&(d.context="inputs"===h.stage?"s":this.assign(this.nextId(),this.getHasOwnProperty("l",a.name)+"?l:s"),d.computed=!1,d.name=a.name);h.if_("inputs"===h.stage||h.not(h.getHasOwnProperty("l",a.name)),function(){h.if_("inputs"===h.stage||"s",function(){e&&1!==e&&h.if_(h.isNull(h.nonComputedMember("s",a.name)),h.lazyAssign(h.nonComputedMember("s",a.name),"{}"));h.assign(b,h.nonComputedMember("s",a.name))})},b&&h.lazyAssign(b,h.nonComputedMember("l",
+a.name)));c(b);break;case q.MemberExpression:g=d&&(d.context=this.nextId())||this.nextId();b=b||this.nextId();h.recurse(a.object,g,void 0,function(){h.if_(h.notNull(g),function(){a.computed?(k=h.nextId(),h.recurse(a.property,k),h.getStringValue(k),e&&1!==e&&h.if_(h.not(h.computedMember(g,k)),h.lazyAssign(h.computedMember(g,k),"{}")),m=h.computedMember(g,k),h.assign(b,m),d&&(d.computed=!0,d.name=k)):(e&&1!==e&&h.if_(h.isNull(h.nonComputedMember(g,a.property.name)),h.lazyAssign(h.nonComputedMember(g,
+a.property.name),"{}")),m=h.nonComputedMember(g,a.property.name),h.assign(b,m),d&&(d.computed=!1,d.name=a.property.name))},function(){h.assign(b,"undefined")});c(b)},!!e);break;case q.CallExpression:b=b||this.nextId();a.filter?(k=h.filter(a.callee.name),l=[],r(a.arguments,function(a){var b=h.nextId();h.recurse(a,b);l.push(b)}),m=k+"("+l.join(",")+")",h.assign(b,m),c(b)):(k=h.nextId(),g={},l=[],h.recurse(a.callee,k,g,function(){h.if_(h.notNull(k),function(){r(a.arguments,function(b){h.recurse(b,a.constant?
+void 0:h.nextId(),void 0,function(a){l.push(a)})});m=g.name?h.member(g.context,g.name,g.computed)+"("+l.join(",")+")":k+"("+l.join(",")+")";h.assign(b,m)},function(){h.assign(b,"undefined")});c(b)}));break;case q.AssignmentExpression:k=this.nextId();g={};this.recurse(a.left,void 0,g,function(){h.if_(h.notNull(g.context),function(){h.recurse(a.right,k);m=h.member(g.context,g.name,g.computed)+a.operator+k;h.assign(b,m);c(b||m)})},1);break;case q.ArrayExpression:l=[];r(a.elements,function(b){h.recurse(b,
+a.constant?void 0:h.nextId(),void 0,function(a){l.push(a)})});m="["+l.join(",")+"]";this.assign(b,m);c(b||m);break;case q.ObjectExpression:l=[];p=!1;r(a.properties,function(a){a.computed&&(p=!0)});p?(b=b||this.nextId(),this.assign(b,"{}"),r(a.properties,function(a){a.computed?(g=h.nextId(),h.recurse(a.key,g)):g=a.key.type===q.Identifier?a.key.name:""+a.key.value;k=h.nextId();h.recurse(a.value,k);h.assign(h.member(b,g,a.computed),k)})):(r(a.properties,function(b){h.recurse(b.value,a.constant?void 0:
+h.nextId(),void 0,function(a){l.push(h.escape(b.key.type===q.Identifier?b.key.name:""+b.key.value)+":"+a)})}),m="{"+l.join(",")+"}",this.assign(b,m));c(b||m);break;case q.ThisExpression:this.assign(b,"s");c(b||"s");break;case q.LocalsExpression:this.assign(b,"l");c(b||"l");break;case q.NGValueParameter:this.assign(b,"v"),c(b||"v")}},getHasOwnProperty:function(a,b){var d=a+"."+b,c=this.current().own;c.hasOwnProperty(d)||(c[d]=this.nextId(!1,a+"&&("+this.escape(b)+" in "+a+")"));return c[d]},assign:function(a,
+b){if(a)return this.current().body.push(a,"=",b,";"),a},filter:function(a){this.state.filters.hasOwnProperty(a)||(this.state.filters[a]=this.nextId(!0));return this.state.filters[a]},ifDefined:function(a,b){return"ifDefined("+a+","+this.escape(b)+")"},plus:function(a,b){return"plus("+a+","+b+")"},return_:function(a){this.current().body.push("return ",a,";")},if_:function(a,b,d){if(!0===a)b();else{var c=this.current().body;c.push("if(",a,"){");b();c.push("}");d&&(c.push("else{"),d(),c.push("}"))}},
+not:function(a){return"!("+a+")"},isNull:function(a){return a+"==null"},notNull:function(a){return a+"!=null"},nonComputedMember:function(a,b){var d=/[^$_a-zA-Z0-9]/g;return/^[$_a-zA-Z][$_a-zA-Z0-9]*$/.test(b)?a+"."+b:a+'["'+b.replace(d,this.stringEscapeFn)+'"]'},computedMember:function(a,b){return a+"["+b+"]"},member:function(a,b,d){return d?this.computedMember(a,b):this.nonComputedMember(a,b)},getStringValue:function(a){this.assign(a,"getStringValue("+a+")")},lazyRecurse:function(a,b,d,c,e,f){var g=
+this;return function(){g.recurse(a,b,d,c,e,f)}},lazyAssign:function(a,b){var d=this;return function(){d.assign(a,b)}},stringEscapeRegex:/[^ a-zA-Z0-9]/g,stringEscapeFn:function(a){return"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)},escape:function(a){if(A(a))return"'"+a.replace(this.stringEscapeRegex,this.stringEscapeFn)+"'";if(W(a))return a.toString();if(!0===a)return"true";if(!1===a)return"false";if(null===a)return"null";if("undefined"===typeof a)return"undefined";throw Ya("esc");},nextId:function(a,
+b){var d="v"+this.state.nextId++;a||this.current().vars.push(d+(b?"="+b:""));return d},current:function(){return this.state[this.state.computing]}};Kd.prototype={compile:function(a){var b=this;Z(a,b.$filter);var d,c;if(d=Id(a))c=this.recurse(d);d=Gd(a.body);var e;d&&(e=[],r(d,function(a,c){var d=b.recurse(a);d.isPure=a.isPure;a.input=d;e.push(d);a.watchId=c}));var f=[];r(a.body,function(a){f.push(b.recurse(a.expression))});a=0===a.body.length?E:1===a.body.length?f[0]:function(a,b){var c;r(f,function(d){c=
+d(a,b)});return c};c&&(a.assign=function(a,b,d){return c(a,d,b)});e&&(a.inputs=e);return a},recurse:function(a,b,d){var c,e,f=this,g;if(a.input)return this.inputs(a.input,a.watchId);switch(a.type){case q.Literal:return this.value(a.value,b);case q.UnaryExpression:return e=this.recurse(a.argument),this["unary"+a.operator](e,b);case q.BinaryExpression:return c=this.recurse(a.left),e=this.recurse(a.right),this["binary"+a.operator](c,e,b);case q.LogicalExpression:return c=this.recurse(a.left),e=this.recurse(a.right),
+this["binary"+a.operator](c,e,b);case q.ConditionalExpression:return this["ternary?:"](this.recurse(a.test),this.recurse(a.alternate),this.recurse(a.consequent),b);case q.Identifier:return f.identifier(a.name,b,d);case q.MemberExpression:return c=this.recurse(a.object,!1,!!d),a.computed||(e=a.property.name),a.computed&&(e=this.recurse(a.property)),a.computed?this.computedMember(c,e,b,d):this.nonComputedMember(c,e,b,d);case q.CallExpression:return g=[],r(a.arguments,function(a){g.push(f.recurse(a))}),
+a.filter&&(e=this.$filter(a.callee.name)),a.filter||(e=this.recurse(a.callee,!0)),a.filter?function(a,c,d,f){for(var p=[],n=0;n":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)>b(c,e,f,g);return d?{value:c}:c}},"binary<=":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)<=b(c,e,f,g);return d?{value:c}:c}},"binary>=":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)>=b(c,e,f,g);return d?{value:c}:c}},"binary&&":function(a,b,d){return function(c,e,f,g){c=
+a(c,e,f,g)&&b(c,e,f,g);return d?{value:c}:c}},"binary||":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)||b(c,e,f,g);return d?{value:c}:c}},"ternary?:":function(a,b,d,c){return function(e,f,g,k){e=a(e,f,g,k)?b(e,f,g,k):d(e,f,g,k);return c?{value:e}:e}},value:function(a,b){return function(){return b?{context:void 0,name:void 0,value:a}:a}},identifier:function(a,b,d){return function(c,e,f,g){c=e&&a in e?e:c;d&&1!==d&&c&&null==c[a]&&(c[a]={});e=c?c[a]:void 0;return b?{context:c,name:a,value:e}:
+e}},computedMember:function(a,b,d,c){return function(e,f,g,k){var h=a(e,f,g,k),l,m;null!=h&&(l=b(e,f,g,k),l+="",c&&1!==c&&h&&!h[l]&&(h[l]={}),m=h[l]);return d?{context:h,name:l,value:m}:m}},nonComputedMember:function(a,b,d,c){return function(e,f,g,k){e=a(e,f,g,k);c&&1!==c&&e&&null==e[b]&&(e[b]={});f=null!=e?e[b]:void 0;return d?{context:e,name:b,value:f}:f}},inputs:function(a,b){return function(d,c,e,f){return f?f[b]:a(d,c,e)}}};Mb.prototype={constructor:Mb,parse:function(a){a=this.getAst(a);var b=
+this.astCompiler.compile(a.ast),d=a.ast;b.literal=0===d.body.length||1===d.body.length&&(d.body[0].expression.type===q.Literal||d.body[0].expression.type===q.ArrayExpression||d.body[0].expression.type===q.ObjectExpression);b.constant=a.ast.constant;b.oneTime=a.oneTime;return b},getAst:function(a){var b=!1;a=a.trim();":"===a.charAt(0)&&":"===a.charAt(1)&&(b=!0,a=a.substring(2));return{ast:this.ast.ast(a),oneTime:b}}};var Ea=F("$sce"),V={HTML:"html",CSS:"css",MEDIA_URL:"mediaUrl",URL:"url",RESOURCE_URL:"resourceUrl",
+JS:"js"},Cc=/_([a-z])/g,Ug=F("$templateRequest"),Vg=F("$timeout"),aa=C.document.createElement("a"),Od=ga(C.location.href),Na;aa.href="http://[::1]";var Wg="[::1]"===aa.hostname;Pd.$inject=["$document"];dd.$inject=["$provide"];var Wd=22,Vd=".",Ec="0";Qd.$inject=["$locale"];Sd.$inject=["$locale"];var gh={yyyy:ea("FullYear",4,0,!1,!0),yy:ea("FullYear",2,0,!0,!0),y:ea("FullYear",1,0,!1,!0),MMMM:kb("Month"),MMM:kb("Month",!0),MM:ea("Month",2,1),M:ea("Month",1,1),LLLL:kb("Month",!1,!0),dd:ea("Date",2),
+d:ea("Date",1),HH:ea("Hours",2),H:ea("Hours",1),hh:ea("Hours",2,-12),h:ea("Hours",1,-12),mm:ea("Minutes",2),m:ea("Minutes",1),ss:ea("Seconds",2),s:ea("Seconds",1),sss:ea("Milliseconds",3),EEEE:kb("Day"),EEE:kb("Day",!0),a:function(a,b){return 12>a.getHours()?b.AMPMS[0]:b.AMPMS[1]},Z:function(a,b,d){a=-1*d;return a=(0<=a?"+":"")+(Ob(Math[0=a.getFullYear()?b.ERANAMES[0]:b.ERANAMES[1]}},
+fh=/((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))([\s\S]*)/,eh=/^-?\d+$/;Rd.$inject=["$locale"];var $g=ia(K),ah=ia(ub);Td.$inject=["$parse"];var Me=ia({restrict:"E",compile:function(a,b){if(!b.href&&!b.xlinkHref)return function(a,b){if("a"===b[0].nodeName.toLowerCase()){var e="[object SVGAnimatedString]"===la.call(b.prop("href"))?"xlink:href":"href";b.on("click",function(a){b.attr(e)||a.preventDefault()})}}}}),vb={};r(Gb,function(a,b){function d(a,d,e){a.$watch(e[c],
+function(a){e.$set(b,!!a)})}if("multiple"!==a){var c=wa("ng-"+b),e=d;"checked"===a&&(e=function(a,b,e){e.ngModel!==e[c]&&d(a,b,e)});vb[c]=function(){return{restrict:"A",priority:100,link:e}}}});r(td,function(a,b){vb[b]=function(){return{priority:100,link:function(a,c,e){if("ngPattern"===b&&"/"===e.ngPattern.charAt(0)&&(c=e.ngPattern.match(ie))){e.$set("ngPattern",new RegExp(c[1],c[2]));return}a.$watch(e[b],function(a){e.$set(b,a)})}}}});r(["src","srcset","href"],function(a){var b=wa("ng-"+a);vb[b]=
+["$sce",function(d){return{priority:99,link:function(c,e,f){var g=a,k=a;"href"===a&&"[object SVGAnimatedString]"===la.call(e.prop("href"))&&(k="xlinkHref",f.$attr[k]="xlink:href",g=null);f.$set(b,d.getTrustedMediaUrl(f[b]));f.$observe(b,function(b){b?(f.$set(k,b),Ca&&g&&e.prop(g,f[k])):"href"===a&&f.$set(k,null)})}}}]});var lb={$addControl:E,$getControls:ia([]),$$renameControl:function(a,b){a.$name=b},$removeControl:E,$setValidity:E,$setDirty:E,$setPristine:E,$setSubmitted:E,$$setSubmitted:E};Pb.$inject=
+["$element","$attrs","$scope","$animate","$interpolate"];Pb.prototype={$rollbackViewValue:function(){r(this.$$controls,function(a){a.$rollbackViewValue()})},$commitViewValue:function(){r(this.$$controls,function(a){a.$commitViewValue()})},$addControl:function(a){Ja(a.$name,"input");this.$$controls.push(a);a.$name&&(this[a.$name]=a);a.$$parentForm=this},$getControls:function(){return ja(this.$$controls)},$$renameControl:function(a,b){var d=a.$name;this[d]===a&&delete this[d];this[b]=a;a.$name=b},$removeControl:function(a){a.$name&&
+this[a.$name]===a&&delete this[a.$name];r(this.$pending,function(b,d){this.$setValidity(d,null,a)},this);r(this.$error,function(b,d){this.$setValidity(d,null,a)},this);r(this.$$success,function(b,d){this.$setValidity(d,null,a)},this);cb(this.$$controls,a);a.$$parentForm=lb},$setDirty:function(){this.$$animate.removeClass(this.$$element,Za);this.$$animate.addClass(this.$$element,Vb);this.$dirty=!0;this.$pristine=!1;this.$$parentForm.$setDirty()},$setPristine:function(){this.$$animate.setClass(this.$$element,
+Za,Vb+" ng-submitted");this.$dirty=!1;this.$pristine=!0;this.$submitted=!1;r(this.$$controls,function(a){a.$setPristine()})},$setUntouched:function(){r(this.$$controls,function(a){a.$setUntouched()})},$setSubmitted:function(){for(var a=this;a.$$parentForm&&a.$$parentForm!==lb;)a=a.$$parentForm;a.$$setSubmitted()},$$setSubmitted:function(){this.$$animate.addClass(this.$$element,"ng-submitted");this.$submitted=!0;r(this.$$controls,function(a){a.$$setSubmitted&&a.$$setSubmitted()})}};ae({clazz:Pb,set:function(a,
+b,d){var c=a[b];c?-1===c.indexOf(d)&&c.push(d):a[b]=[d]},unset:function(a,b,d){var c=a[b];c&&(cb(c,d),0===c.length&&delete a[b])}});var ke=function(a){return["$timeout","$parse",function(b,d){function c(a){return""===a?d('this[""]').assign:d(a).assign||E}return{name:"form",restrict:a?"EAC":"E",require:["form","^^?form"],controller:Pb,compile:function(d,f){d.addClass(Za).addClass(mb);var g=f.name?"name":a&&f.ngForm?"ngForm":!1;return{pre:function(a,d,e,f){var p=f[0];if(!("action"in e)){var n=function(b){a.$apply(function(){p.$commitViewValue();
+p.$setSubmitted()});b.preventDefault()};d[0].addEventListener("submit",n);d.on("$destroy",function(){b(function(){d[0].removeEventListener("submit",n)},0,!1)})}(f[1]||p.$$parentForm).$addControl(p);var s=g?c(p.$name):E;g&&(s(a,p),e.$observe(g,function(b){p.$name!==b&&(s(a,void 0),p.$$parentForm.$$renameControl(p,b),s=c(p.$name),s(a,p))}));d.on("$destroy",function(){p.$$parentForm.$removeControl(p);s(a,void 0);S(p,lb)})}}}}}]},Ne=ke(),Ze=ke(!0),hh=/^\d{4,}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+(?:[+-][0-2]\d:[0-5]\d|Z)$/,
+sh=/^[a-z][a-z\d.+-]*:\/*(?:[^:@]+(?::[^@]+)?@)?(?:[^\s:/?#]+|\[[a-f\d:]+])(?::\d+)?(?:\/[^?#]*)?(?:\?[^#]*)?(?:#.*)?$/i,th=/^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/,ih=/^\s*(-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/,le=/^(\d{4,})-(\d{2})-(\d{2})$/,me=/^(\d{4,})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/,Mc=/^(\d{4,})-W(\d\d)$/,ne=/^(\d{4,})-(\d\d)$/,
+oe=/^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/,ce=T();r(["date","datetime-local","month","time","week"],function(a){ce[a]=!0});var pe={text:function(a,b,d,c,e,f){Sa(a,b,d,c,e,f);Hc(c)},date:nb("date",le,Qb(le,["yyyy","MM","dd"]),"yyyy-MM-dd"),"datetime-local":nb("datetimelocal",me,Qb(me,"yyyy MM dd HH mm ss sss".split(" ")),"yyyy-MM-ddTHH:mm:ss.sss"),time:nb("time",oe,Qb(oe,["HH","mm","ss","sss"]),"HH:mm:ss.sss"),week:nb("week",Mc,function(a,b){if(ha(a))return a;if(A(a)){Mc.lastIndex=0;var d=Mc.exec(a);
+if(d){var c=+d[1],e=+d[2],f=d=0,g=0,k=0,h=Xd(c),e=7*(e-1);b&&(d=b.getHours(),f=b.getMinutes(),g=b.getSeconds(),k=b.getMilliseconds());return new Date(c,0,h.getDate()+e,d,f,g,k)}}return NaN},"yyyy-Www"),month:nb("month",ne,Qb(ne,["yyyy","MM"]),"yyyy-MM"),number:function(a,b,d,c,e,f,g,k){Ic(a,b,d,c,"number");de(c);Sa(a,b,d,c,e,f);var h;if(w(d.min)||d.ngMin){var l=d.min||k(d.ngMin)(a);h=na(l);c.$validators.min=function(a,b){return c.$isEmpty(b)||z(h)||b>=h};d.$observe("min",function(a){a!==l&&(h=na(a),
+l=a,c.$validate())})}if(w(d.max)||d.ngMax){var m=d.max||k(d.ngMax)(a),p=na(m);c.$validators.max=function(a,b){return c.$isEmpty(b)||z(p)||b<=p};d.$observe("max",function(a){a!==m&&(p=na(a),m=a,c.$validate())})}if(w(d.step)||d.ngStep){var n=d.step||k(d.ngStep)(a),s=na(n);c.$validators.step=function(a,b){return c.$isEmpty(b)||z(s)||ee(b,h||0,s)};d.$observe("step",function(a){a!==n&&(s=na(a),n=a,c.$validate())})}},url:function(a,b,d,c,e,f){Sa(a,b,d,c,e,f);Hc(c);c.$validators.url=function(a,b){var d=
+a||b;return c.$isEmpty(d)||sh.test(d)}},email:function(a,b,d,c,e,f){Sa(a,b,d,c,e,f);Hc(c);c.$validators.email=function(a,b){var d=a||b;return c.$isEmpty(d)||th.test(d)}},radio:function(a,b,d,c){var e=!d.ngTrim||"false"!==U(d.ngTrim);z(d.name)&&b.attr("name",++pb);b.on("change",function(a){var g;b[0].checked&&(g=d.value,e&&(g=U(g)),c.$setViewValue(g,a&&a.type))});c.$render=function(){var a=d.value;e&&(a=U(a));b[0].checked=a===c.$viewValue};d.$observe("value",c.$render)},range:function(a,b,d,c,e,f){function g(a,
+c){b.attr(a,d[a]);var e=d[a];d.$observe(a,function(a){a!==e&&(e=a,c(a))})}function k(a){p=na(a);X(c.$modelValue)||(m?(a=b.val(),p>a&&(a=p,b.val(a)),c.$setViewValue(a)):c.$validate())}function h(a){n=na(a);X(c.$modelValue)||(m?(a=b.val(),n =p},g("min",k));e&&(n=na(d.max),c.$validators.max=m?function(){return!0}:function(a,b){return c.$isEmpty(b)||z(n)||b<=n},g("max",h));f&&(s=na(d.step),c.$validators.step=m?function(){return!r.stepMismatch}:
+function(a,b){return c.$isEmpty(b)||z(s)||ee(b,p||0,s)},g("step",l))},checkbox:function(a,b,d,c,e,f,g,k){var h=fe(k,a,"ngTrueValue",d.ngTrueValue,!0),l=fe(k,a,"ngFalseValue",d.ngFalseValue,!1);b.on("change",function(a){c.$setViewValue(b[0].checked,a&&a.type)});c.$render=function(){b[0].checked=c.$viewValue};c.$isEmpty=function(a){return!1===a};c.$formatters.push(function(a){return va(a,h)});c.$parsers.push(function(a){return a?h:l})},hidden:E,button:E,submit:E,reset:E,file:E},Yc=["$browser","$sniffer",
+"$filter","$parse",function(a,b,d,c){return{restrict:"E",require:["?ngModel"],link:{pre:function(e,f,g,k){k[0]&&(pe[K(g.type)]||pe.text)(e,f,g,k[0],b,a,d,c)}}}}],vf=function(){var a={configurable:!0,enumerable:!1,get:function(){return this.getAttribute("value")||""},set:function(a){this.setAttribute("value",a)}};return{restrict:"E",priority:200,compile:function(b,d){if("hidden"===K(d.type))return{pre:function(b,d,f,g){b=d[0];b.parentNode&&b.parentNode.insertBefore(b,b.nextSibling);Object.defineProperty&&
+Object.defineProperty(b,"value",a)}}}}},uh=/^(true|false|\d+)$/,sf=function(){function a(a,d,c){var e=w(c)?c:9===Ca?"":null;a.prop("value",e);d.$set("value",c)}return{restrict:"A",priority:100,compile:function(b,d){return uh.test(d.ngValue)?function(b,d,f){b=b.$eval(f.ngValue);a(d,f,b)}:function(b,d,f){b.$watch(f.ngValue,function(b){a(d,f,b)})}}}},Re=["$compile",function(a){return{restrict:"AC",compile:function(b){a.$$addBindingClass(b);return function(b,c,e){a.$$addBindingInfo(c,e.ngBind);c=c[0];
+b.$watch(e.ngBind,function(a){c.textContent=ic(a)})}}}}],Te=["$interpolate","$compile",function(a,b){return{compile:function(d){b.$$addBindingClass(d);return function(c,d,f){c=a(d.attr(f.$attr.ngBindTemplate));b.$$addBindingInfo(d,c.expressions);d=d[0];f.$observe("ngBindTemplate",function(a){d.textContent=z(a)?"":a})}}}}],Se=["$sce","$parse","$compile",function(a,b,d){return{restrict:"A",compile:function(c,e){var f=b(e.ngBindHtml),g=b(e.ngBindHtml,function(b){return a.valueOf(b)});d.$$addBindingClass(c);
+return function(b,c,e){d.$$addBindingInfo(c,e.ngBindHtml);b.$watch(g,function(){var d=f(b);c.html(a.getTrustedHtml(d)||"")})}}}}],rf=ia({restrict:"A",require:"ngModel",link:function(a,b,d,c){c.$viewChangeListeners.push(function(){a.$eval(d.ngChange)})}}),Ue=Kc("",!0),We=Kc("Odd",0),Ve=Kc("Even",1),Xe=Ra({compile:function(a,b){b.$set("ngCloak",void 0);a.removeClass("ng-cloak")}}),Ye=[function(){return{restrict:"A",scope:!0,controller:"@",priority:500}}],cd={},vh={blur:!0,focus:!0};r("click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste".split(" "),
+function(a){var b=wa("ng-"+a);cd[b]=["$parse","$rootScope","$exceptionHandler",function(d,c,e){return qd(d,c,e,b,a,vh[a])}]});var af=["$animate","$compile",function(a,b){return{multiElement:!0,transclude:"element",priority:600,terminal:!0,restrict:"A",$$tlb:!0,link:function(d,c,e,f,g){var k,h,l;d.$watch(e.ngIf,function(d){d?h||g(function(d,f){h=f;d[d.length++]=b.$$createComment("end ngIf",e.ngIf);k={clone:d};a.enter(d,c.parent(),c)}):(l&&(l.remove(),l=null),h&&(h.$destroy(),h=null),k&&(l=tb(k.clone),
+a.leave(l).done(function(a){!1!==a&&(l=null)}),k=null))})}}}],bf=["$templateRequest","$anchorScroll","$animate",function(a,b,d){return{restrict:"ECA",priority:400,terminal:!0,transclude:"element",controller:ca.noop,compile:function(c,e){var f=e.ngInclude||e.src,g=e.onload||"",k=e.autoscroll;return function(c,e,m,p,n){var r=0,q,t,x,v=function(){t&&(t.remove(),t=null);q&&(q.$destroy(),q=null);x&&(d.leave(x).done(function(a){!1!==a&&(t=null)}),t=x,x=null)};c.$watch(f,function(f){var m=function(a){!1===
+a||!w(k)||k&&!c.$eval(k)||b()},t=++r;f?(a(f,!0).then(function(a){if(!c.$$destroyed&&t===r){var b=c.$new();p.template=a;a=n(b,function(a){v();d.enter(a,null,e).done(m)});q=b;x=a;q.$emit("$includeContentLoaded",f);c.$eval(g)}},function(){c.$$destroyed||t!==r||(v(),c.$emit("$includeContentError",f))}),c.$emit("$includeContentRequested",f)):(v(),p.template=null)})}}}}],uf=["$compile",function(a){return{restrict:"ECA",priority:-400,require:"ngInclude",link:function(b,d,c,e){la.call(d[0]).match(/SVG/)?
+(d.empty(),a(ed(e.template,C.document).childNodes)(b,function(a){d.append(a)},{futureParentElement:d})):(d.html(e.template),a(d.contents())(b))}}}],cf=Ra({priority:450,compile:function(){return{pre:function(a,b,d){a.$eval(d.ngInit)}}}}),qf=function(){return{restrict:"A",priority:100,require:"ngModel",link:function(a,b,d,c){var e=d.ngList||", ",f="false"!==d.ngTrim,g=f?U(e):e;c.$parsers.push(function(a){if(!z(a)){var b=[];a&&r(a.split(g),function(a){a&&b.push(f?U(a):a)});return b}});c.$formatters.push(function(a){if(H(a))return a.join(e)});
+c.$isEmpty=function(a){return!a||!a.length}}}},mb="ng-valid",$d="ng-invalid",Za="ng-pristine",Vb="ng-dirty",ob=F("ngModel");Rb.$inject="$scope $exceptionHandler $attrs $element $parse $animate $timeout $q $interpolate".split(" ");Rb.prototype={$$initGetterSetters:function(){if(this.$options.getOption("getterSetter")){var a=this.$$parse(this.$$attr.ngModel+"()"),b=this.$$parse(this.$$attr.ngModel+"($$$p)");this.$$ngModelGet=function(b){var c=this.$$parsedNgModel(b);B(c)&&(c=a(b));return c};this.$$ngModelSet=
+function(a,c){B(this.$$parsedNgModel(a))?b(a,{$$$p:c}):this.$$parsedNgModelAssign(a,c)}}else if(!this.$$parsedNgModel.assign)throw ob("nonassign",this.$$attr.ngModel,za(this.$$element));},$render:E,$isEmpty:function(a){return z(a)||""===a||null===a||a!==a},$$updateEmptyClasses:function(a){this.$isEmpty(a)?(this.$$animate.removeClass(this.$$element,"ng-not-empty"),this.$$animate.addClass(this.$$element,"ng-empty")):(this.$$animate.removeClass(this.$$element,"ng-empty"),this.$$animate.addClass(this.$$element,
+"ng-not-empty"))},$setPristine:function(){this.$dirty=!1;this.$pristine=!0;this.$$animate.removeClass(this.$$element,Vb);this.$$animate.addClass(this.$$element,Za)},$setDirty:function(){this.$dirty=!0;this.$pristine=!1;this.$$animate.removeClass(this.$$element,Za);this.$$animate.addClass(this.$$element,Vb);this.$$parentForm.$setDirty()},$setUntouched:function(){this.$touched=!1;this.$untouched=!0;this.$$animate.setClass(this.$$element,"ng-untouched","ng-touched")},$setTouched:function(){this.$touched=
+!0;this.$untouched=!1;this.$$animate.setClass(this.$$element,"ng-touched","ng-untouched")},$rollbackViewValue:function(){this.$$timeout.cancel(this.$$pendingDebounce);this.$viewValue=this.$$lastCommittedViewValue;this.$render()},$validate:function(){if(!X(this.$modelValue)){var a=this.$$lastCommittedViewValue,b=this.$$rawModelValue,d=this.$valid,c=this.$modelValue,e=this.$options.getOption("allowInvalid"),f=this;this.$$runValidators(b,a,function(a){e||d===a||(f.$modelValue=a?b:void 0,f.$modelValue!==
+c&&f.$$writeModelToScope())})}},$$runValidators:function(a,b,d){function c(){var c=!0;r(h.$validators,function(d,e){var g=Boolean(d(a,b));c=c&&g;f(e,g)});return c?!0:(r(h.$asyncValidators,function(a,b){f(b,null)}),!1)}function e(){var c=[],d=!0;r(h.$asyncValidators,function(e,g){var h=e(a,b);if(!h||!B(h.then))throw ob("nopromise",h);f(g,void 0);c.push(h.then(function(){f(g,!0)},function(){d=!1;f(g,!1)}))});c.length?h.$$q.all(c).then(function(){g(d)},E):g(!0)}function f(a,b){k===h.$$currentValidationRunId&&
+h.$setValidity(a,b)}function g(a){k===h.$$currentValidationRunId&&d(a)}this.$$currentValidationRunId++;var k=this.$$currentValidationRunId,h=this;(function(){var a=h.$$parserName;if(z(h.$$parserValid))f(a,null);else return h.$$parserValid||(r(h.$validators,function(a,b){f(b,null)}),r(h.$asyncValidators,function(a,b){f(b,null)})),f(a,h.$$parserValid),h.$$parserValid;return!0})()?c()?e():g(!1):g(!1)},$commitViewValue:function(){var a=this.$viewValue;this.$$timeout.cancel(this.$$pendingDebounce);if(this.$$lastCommittedViewValue!==
+a||""===a&&this.$$hasNativeValidators)this.$$updateEmptyClasses(a),this.$$lastCommittedViewValue=a,this.$pristine&&this.$setDirty(),this.$$parseAndValidate()},$$parseAndValidate:function(){var a=this.$$lastCommittedViewValue,b=this;this.$$parserValid=z(a)?void 0:!0;this.$setValidity(this.$$parserName,null);this.$$parserName="parse";if(this.$$parserValid)for(var d=0;dg||e.$isEmpty(b)||b.length<=g}}}}}],ad=["$parse",function(a){return{restrict:"A",require:"?ngModel",link:function(b,d,c,e){if(e){var f=c.minlength||a(c.ngMinlength)(b),g=Tb(f)||-1;c.$observe("minlength",function(a){f!==
+a&&(g=Tb(a)||-1,f=a,e.$validate())});e.$validators.minlength=function(a,b){return e.$isEmpty(b)||b.length>=g}}}}}];C.angular.bootstrap?C.console&&console.log("WARNING: Tried to load AngularJS more than once."):(Fe(),Je(ca),ca.module("ngLocale",[],["$provide",function(a){function b(a){a+="";var b=a.indexOf(".");return-1==b?0:a.length-b-1}a.value("$locale",{DATETIME_FORMATS:{AMPMS:["AM","PM"],DAY:"Sunday Monday Tuesday Wednesday Thursday Friday Saturday".split(" "),ERANAMES:["Before Christ","Anno Domini"],
+ERAS:["BC","AD"],FIRSTDAYOFWEEK:6,MONTH:"January February March April May June July August September October November December".split(" "),SHORTDAY:"Sun Mon Tue Wed Thu Fri Sat".split(" "),SHORTMONTH:"Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" "),STANDALONEMONTH:"January February March April May June July August September October November December".split(" "),WEEKENDRANGE:[5,6],fullDate:"EEEE, MMMM d, y",longDate:"MMMM d, y",medium:"MMM d, y h:mm:ss a",mediumDate:"MMM d, y",mediumTime:"h:mm:ss a",
+"short":"M/d/yy h:mm a",shortDate:"M/d/yy",shortTime:"h:mm a"},NUMBER_FORMATS:{CURRENCY_SYM:"$",DECIMAL_SEP:".",GROUP_SEP:",",PATTERNS:[{gSize:3,lgSize:3,maxFrac:3,minFrac:0,minInt:1,negPre:"-",negSuf:"",posPre:"",posSuf:""},{gSize:3,lgSize:3,maxFrac:2,minFrac:2,minInt:1,negPre:"-\u00a4",negSuf:"",posPre:"\u00a4",posSuf:""}]},id:"en-us",localeID:"en_US",pluralCat:function(a,c){var e=a|0,f=c;void 0===f&&(f=Math.min(b(a),3));Math.pow(10,f);return 1==e&&0==f?"one":"other"}})}]),x(function(){Ae(C.document,
+Uc)}))})(window);!window.angular.$$csp().noInlineStyle&&window.angular.element(document.head).prepend('');
+//# sourceMappingURL=angular.min.js.map
diff --git a/admin/static/js/app.js b/admin/static/js/app.js
index 77d1b215..9b6fc13b 100644
--- a/admin/static/js/app.js
+++ b/admin/static/js/app.js
@@ -1,413 +1,2988 @@
-angular.module("FICApp", ["ngRoute", "ngResource"])
- .config(function($routeProvider, $locationProvider) {
- $routeProvider
- .when("/themes", {
- controller: "ThemesListController",
- templateUrl: "views/theme-list.html"
- })
- .when("/themes/:themeId", {
- controller: "ThemeController",
- templateUrl: "views/theme.html"
- })
- .when("/themes/:themeId/:exerciceId", {
- controller: "ExerciceController",
- templateUrl: "views/exercice.html"
- })
- .when("/teams", {
- controller: "TeamsListController",
- templateUrl: "views/team-list.html"
- })
- .when("/teams/:teamId", {
- controller: "TeamController",
- templateUrl: "views/team.html"
- })
- .when("/teams/new", {
- controller: "TeamNewController",
- templateUrl: "views/team-new.html"
- })
- .when("/", {
- templateUrl: "views/home.html"
- });
- $locationProvider.html5Mode(true);
- });
+angular.module("FICApp", ["ngRoute", "ngResource", "ngSanitize"])
+ .config(function ($routeProvider, $locationProvider) {
+ $routeProvider
+ .when("/themes", {
+ controller: "ThemesListController",
+ templateUrl: "views/theme-list.html"
+ })
+ .when("/themes/:themeId", {
+ controller: "ThemeController",
+ templateUrl: "views/theme.html"
+ })
+ .when("/themes/:themeId/exercices/:exerciceId", {
+ controller: "ExerciceController",
+ templateUrl: "views/exercice.html"
+ })
+ .when("/repositories", {
+ controller: "RepositoriesController",
+ templateUrl: "views/repositories.html"
+ })
+ .when("/sync", {
+ controller: "SyncController",
+ templateUrl: "views/sync.html"
+ })
+ .when("/settings", {
+ controller: "SettingsController",
+ templateUrl: "views/settings.html"
+ })
+ .when("/auth", {
+ controller: "AuthController",
+ templateUrl: "views/auth.html"
+ })
+ .when("/pki", {
+ controller: "PKIController",
+ templateUrl: "views/pki.html"
+ })
+ .when("/exercices", {
+ controller: "AllExercicesListController",
+ templateUrl: "views/exercice-list.html"
+ })
+ .when("/exercices/:exerciceId", {
+ controller: "ExerciceController",
+ templateUrl: "views/exercice.html"
+ })
+ .when("/exercices/:exerciceId/flags", {
+ controller: "ExerciceController",
+ templateUrl: "views/exercice-flags.html"
+ })
+ .when("/exercices/:exerciceId/resolution", {
+ controller: "ExerciceController",
+ templateUrl: "views/exercice-resolution.html"
+ })
+ .when("/forge-links", {
+ controller: "ForgeLinksController",
+ templateUrl: "views/exercices-forgelink.html"
+ })
+ .when("/tags", {
+ controller: "TagsListController",
+ templateUrl: "views/tags.html"
+ })
+ .when("/teams", {
+ controller: "TeamsListController",
+ templateUrl: "views/team-list.html"
+ })
+ .when("/teams/print", {
+ controller: "TeamsListController",
+ templateUrl: "views/team-print.html"
+ })
+ .when("/teams/export", {
+ controller: "TeamsListController",
+ templateUrl: "views/team-export.html"
+ })
+ .when("/teams/:teamId", {
+ controller: "TeamController",
+ templateUrl: "views/team-edit.html"
+ })
+ .when("/teams/:teamId/stats", {
+ controller: "TeamController",
+ templateUrl: "views/team-stats.html"
+ })
+ .when("/teams/:teamId/score", {
+ controller: "TeamController",
+ templateUrl: "views/team-score.html"
+ })
+ .when("/public/:screenId", {
+ controller: "PublicController",
+ templateUrl: "views/public.html"
+ })
+ .when("/files", {
+ controller: "FilesListController",
+ templateUrl: "views/file-list.html"
+ })
+ .when("/events", {
+ controller: "EventsListController",
+ templateUrl: "views/event-list.html"
+ })
+ .when("/events/:eventId", {
+ controller: "EventController",
+ templateUrl: "views/event.html"
+ })
+ .when("/claims", {
+ controller: "ClaimsListController",
+ templateUrl: "views/claim-list.html"
+ })
+ .when("/claims/:claimId", {
+ controller: "ClaimController",
+ templateUrl: "views/claim.html"
+ })
+ .when("/", {
+ templateUrl: "views/home.html"
+ });
+ $locationProvider.html5Mode(true);
+ });
-angular.module("FICApp")
- .factory("Version", function($resource) {
- return $resource("/api/version")
- })
- .factory("Team", function($resource) {
- return $resource("/api/teams/:teamId", { teamId: '@id' }, {
- 'save': {method: 'PATCH'},
- })
- })
- .factory("TeamMember", function($resource) {
- return $resource("/api/teams/:teamId/members", { teamId: '@id' })
- })
- .factory("TeamMy", function($resource) {
- return $resource("/api/teams/:teamId/my.json", { teamId: '@id' })
- })
- .factory("Teams", function($resource) {
- return $resource("/api/teams/teams.json")
- })
- .factory("TeamStats", function($resource) {
- return $resource("/api/teams/:teamId/stats.json", { teamId: '@id' })
- })
- .factory("TeamPresence", function($resource) {
- return $resource("/api/teams/:teamId/tries", { teamId: '@id' })
- })
- .factory("Theme", function($resource) {
- return $resource("/api/themes/:themeId", null, {
- 'save': {method: 'PATCH'},
- })
- })
- .factory("Themes", function($resource) {
- return $resource("/api/themes/themes.json", null, {
- 'get': {method: 'GET'},
- })
- })
- .factory("Exercice", function($resource) {
- return $resource("/api/exercices/:exerciceId")
- });
+function setCookie(name, value, days) {
+ var expires;
-String.prototype.capitalize = function() {
- return this
- .toLowerCase()
- .replace(
- /(^|\s)([a-z])/g,
- function(m,p1,p2) { return p1+p2.toUpperCase(); }
- );
+ if (days) {
+ var date = new Date();
+ date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
+ expires = "; expires=" + date.toGMTString();
+ } else {
+ expires = "";
+ }
+ document.cookie = encodeURIComponent(name) + "=" + encodeURIComponent(value) + expires + "; path=/";
+}
+
+function getCookie(name) {
+ var nameEQ = encodeURIComponent(name) + "=";
+ var ca = document.cookie.split(';');
+ for (var i = 0; i < ca.length; i++) {
+ var c = ca[i];
+ while (c.charAt(0) === ' ')
+ c = c.substring(1, c.length);
+ if (c.indexOf(nameEQ) === 0)
+ return decodeURIComponent(c.substring(nameEQ.length, c.length));
+ }
+ return null;
}
angular.module("FICApp")
- .filter("capitalize", function() {
- return function(input) {
- return input.capitalize();
- }
- })
- .filter("time", function() {
- return function(input) {
- if (input == undefined) {
- return "--";
- } else if (input >= 10) {
- return input;
- } else {
- return "0" + input;
- }
- }
- })
-
- .controller("VersionController", function($scope, Version) {
- $scope.v = Version.get();
- })
-
- .controller("ThemesListController", function($scope, Theme, $location) {
- $scope.themes = Theme.query();
- $scope.fields = ["id", "name"];
-
- $scope.show = function(id) {
- $location.url("/themes/" + id);
- };
- })
- .controller("ThemeController", function($scope, Theme, $routeParams) {
- $scope.theme = Theme.get({ themeId: $routeParams.themeId });
- $scope.fields = ["name"];
-
- $scope.saveTheme = function() {
- this.theme.$save({themeId: this.theme.themeId});
- }
- })
-
- .controller("ExercicesListController", function($scope, Exercice, $routeParams, $location) {
- $scope.exercices = Exercice.query({ themeId: $routeParams.themeId });
- $scope.fields = ["id", "title", "statement", "videoURI"];
-
- $scope.show = function(id) {
- $location.url("/themes/" + $routeParams.themeId + "/" + id);
- };
- })
- .controller("ExerciceController", function($scope, Theme, $routeParams) {
- $scope.exercice = Exercice.get({ themeId: $routeParams.themeId });
- $scope.fields = ["name", "statement", "hint", "videoURI"];
-
- $scope.saveTheme = function() {
- this.exercice.$save({ themeId: this.exercice.themeId, exerciceId: this.exercice.exerciceId});
- }
- })
-
- .controller("TeamsListController", function($scope, Team, $location) {
- $scope.teams = Team.query();
- $scope.fields = ["id", "name", "initialName"];
-
- $scope.show = function(id) {
- $location.url("/teams/" + id);
- };
- })
- .controller("TeamController", function($scope, Team, TeamMember, $routeParams) {
- $scope.team = Team.get({ teamId: $routeParams.teamId });
- $scope.members = TeamMember.query({ teamId: $routeParams.teamId });
- })
- .controller("TeamStatsController", function($scope, TeamStats, $routeParams) {
- $scope.teamstats = TeamStats.get({ teamId: $routeParams.teamId });
- $scope.teamstats.$promise.then(function(res) {
- solvedByLevelPie("#pieLevels", res.levels);
- solvedByThemesPie("#pieThemes", res.themes);
- });
- })
- .controller("TeamExercicesController", function($scope, Teams, Themes, TeamMy, Exercice, $routeParams) {
- $scope.teams = Teams.get();
- $scope.themes = Themes.get();
- $scope.exercices = Exercice.query();
- $scope.my = TeamMy.get({ teamId: $routeParams.teamId });
-
- $scope.teams.$promise.then(function(res){
- $scope.nb_teams = 0;
- $scope.nb_reg_teams = Object.keys(res).length;
- angular.forEach(res, function(team, tid) {
- if (team.rank)
- $scope.nb_teams += 1;
- }, 0);
- });
-
- $scope.my.$promise.then(function(res){
- $scope.solved_exercices = 0;
- angular.forEach(res.exercices, function(exercice, eid) {
- if (exercice.solved) {
- $scope.solved_exercices += 1;
+ .directive('autofocus', ['$timeout', function ($timeout) {
+ return {
+ restrict: 'A',
+ link: function ($scope, $element) {
+ $timeout(function () {
+ $element[0].focus();
+ });
+ }
}
- }, 0);
+ }])
+
+ .component('toast', {
+ bindings: {
+ date: '=',
+ msg: '=',
+ timeout: '=',
+ title: '=',
+ variant: '=',
+ yesNo: '=',
+ onyes: '=',
+ onno: '=',
+ },
+ controller: function ($element) {
+ if (this.timeout === 0)
+ $element.children(0).toast({ autohide: false });
+ else if (!this.timeout && this.timeout !== 0)
+ $element.children(0).toast({ delay: 7000 });
+ else
+ $element.children(0).toast({ delay: this.timeout });
+ $element.children(0).toast('show');
+ $element.children(0).on('hidden.bs.toast', function () {
+ $element.children(0).toast('dispose');
+ });
+ this.yesFunc = function () {
+ $element.children(0).toast('hide');
+ if (this.onyes)
+ this.onyes();
+ }
+ this.noFunc = function () {
+ $element.children(0).toast('hide');
+ if (this.onno)
+ this.onno();
+ }
+ },
+ template: ``
});
- })
- .controller("TeamNewController", function($scope, Team, $location) {
- $scope.contact = new Team({
+angular.module("FICApp")
+ .factory("Version", function ($resource) {
+ return $resource("api/version")
+ })
+ .factory("Timestamp", function ($resource) {
+ return $resource("api/timestamps.json")
+ })
+ .factory("Health", function ($resource) {
+ return $resource("api/health.json")
+ })
+ .factory("Monitor", function ($resource) {
+ return $resource("api/monitor/:machineId", { machineId: '@id' })
+ })
+ .factory("Event", function ($resource) {
+ return $resource("api/events/:eventId", { eventId: '@id' }, {
+ 'update': { method: 'PUT' },
+ })
+ })
+ .factory("Claim", function ($resource) {
+ return $resource("api/claims/:claimId", { claimId: '@id' }, {
+ 'update': { method: 'PUT' },
+ })
+ })
+ .factory("ClaimAssignee", function ($resource) {
+ return $resource("api/claims-assignees/:assigneeId", { assigneeId: '@id' }, {
+ 'update': { method: 'PUT' },
+ })
+ })
+ .factory("Certificate", function ($resource) {
+ return $resource("api/certs/:serial", { serial: '@id' }, {
+ 'update': { method: 'PUT' },
+ })
+ })
+ .factory("CACertificate", function ($resource) {
+ return $resource("api/ca/:serial", { serial: '@id' })
+ })
+ .factory("File", function ($resource) {
+ return $resource("api/files/:fileId", { fileId: '@id' })
+ })
+ .factory("Settings", function ($resource) {
+ return $resource("api/settings.json", null, {
+ 'update': { method: 'PUT' },
+ })
+ })
+ .factory("NextSettings", function ($resource) {
+ return $resource("api/settings-next/:tsId", { tsId: '@id' }, {
+ 'update': { method: 'PUT' },
+ })
+ })
+ .factory("SettingsChallenge", function ($resource) {
+ return $resource("api/challenge.json", null, {
+ 'update': { method: 'PUT' },
+ })
+ })
+ .factory("Scene", function ($resource) {
+ return $resource("api/public/:screenId", { screenId: '@id' }, {
+ 'update': { method: 'PUT' },
+ })
+ })
+ .factory("Team", function ($resource) {
+ return $resource("api/teams/:teamId", { teamId: '@id' }, {
+ 'update': { method: 'PUT' },
+ })
+ })
+ .factory("TeamCertificate", function ($resource) {
+ return $resource("api/teams/:teamId/certificates", { teamId: '@id' })
+ })
+ .factory("TeamAssociation", function ($resource) {
+ return $resource("api/teams/:teamId/associations/:assoc", { teamId: '@teamId', assoc: '@assoc' })
+ })
+ .factory("TeamMember", function ($resource) {
+ return $resource("api/teams/:teamId/members", { teamId: '@id' }, {
+ 'save': { method: 'PUT' },
+ })
+ })
+ .factory("TeamMy", function ($resource) {
+ return $resource("api/teams/:teamId/my.json", { teamId: '@id' })
+ })
+ .factory("Teams", function ($resource) {
+ return $resource("api/teams.json")
+ })
+ .factory("TeamHistory", function ($resource) {
+ return $resource("api/teams/:teamId/history.json", { teamId: '@id' })
+ })
+ .factory("TeamScore", function ($resource) {
+ return $resource("api/teams/:teamId/score-grid.json", { teamId: '@id' })
+ })
+ .factory("TeamStats", function ($resource) {
+ return $resource("api/teams/:teamId/stats.json", { teamId: '@id' })
+ })
+ .factory("TeamPresence", function ($resource) {
+ return $resource("api/teams/:teamId/tries", { teamId: '@id' })
+ })
+ .factory("Theme", function ($resource) {
+ return $resource("api/themes/:themeId", { themeId: '@id' }, {
+ update: { method: 'PUT' }
+ });
+ })
+ .factory("Themes", function ($resource) {
+ return $resource("api/themes.json", null, {
+ 'get': { method: 'GET' },
+ })
+ })
+ .factory("ThemedExercice", function ($resource) {
+ return $resource("api/themes/:themeId/exercices/:exerciceId", { themeId: '@id', exerciceId: '@idExercice' }, {
+ update: { method: 'PUT' }
+ })
+ })
+ .factory("Exercice", function ($resource) {
+ return $resource("api/exercices/:exerciceId", { exerciceId: '@id' }, {
+ update: { method: 'PUT' },
+ patch: { method: 'PATCH' }
+ })
+ })
+ .factory("ExerciceClaims", function ($resource) {
+ return $resource("api/exercices/:exerciceId/claims", { exerciceId: '@idExercice' })
+ })
+ .factory("ExerciceTags", function ($resource) {
+ return $resource("api/exercices/:exerciceId/tags", { exerciceId: '@idExercice' }, {
+ update: { method: 'PUT' }
+ })
+ })
+ .factory("ExerciceHistory", function ($resource) {
+ return $resource("api/exercices/:exerciceId/history.json", { exerciceId: '@id' })
+ })
+ .factory("ExerciceTries", function ($resource) {
+ return $resource("api/exercices/:exerciceId/tries/:tryId", { exerciceId: '@idExercice', tryId: '@id' })
+ })
+ .factory("ExercicesStats", function ($resource) {
+ return $resource("api/exercices_stats.json", { themeId: '@id' })
+ })
+ .factory("ExerciceStats", function ($resource) {
+ return $resource("api/exercices/:exerciceId/stats.json", { exerciceId: '@id' })
+ })
+ .factory("ExerciceFile", function ($resource) {
+ return $resource("api/exercices/:exerciceId/files/:fileId", { exerciceId: '@idExercice', fileId: '@id' }, {
+ update: { method: 'PUT' }
+ })
+ })
+ .factory("ExerciceHint", function ($resource) {
+ return $resource("api/exercices/:exerciceId/hints/:hintId", { exerciceId: '@idExercice', hintId: '@id' }, {
+ update: { method: 'PUT' }
+ })
+ })
+ .factory("ExerciceHintDeps", function ($resource) {
+ return $resource("api/exercices/:exerciceId/hints/:hintId/dependancies", { exerciceId: '@idExercice', hintId: '@id' })
+ })
+ .factory("ExerciceFlag", function ($resource) {
+ return $resource("api/exercices/:exerciceId/flags/:flagId", { exerciceId: '@idExercice', flagId: '@id' }, {
+ update: { method: 'PUT' }
+ })
+ })
+ .factory("ExerciceFlagChoices", function ($resource) {
+ return $resource("api/exercices/:exerciceId/flags/:flagId/choices/:choiceId", { exerciceId: '@idExercice', flagId: '@idFlag', choiceId: '@id' }, {
+ 'update': { method: 'PUT' },
+ })
+ })
+ .factory("ExerciceFlagDeps", function ($resource) {
+ return $resource("api/exercices/:exerciceId/flags/:flagId/dependancies", { exerciceId: '@idExercice', flagId: '@id' })
+ })
+ .factory("ExerciceFlagStats", function ($resource) {
+ return $resource("api/exercices/:exerciceId/flags/:flagId/statistics", { exerciceId: '@idExercice', flagId: '@id' })
+ })
+ .factory("ExerciceMCQFlag", function ($resource) {
+ return $resource("api/exercices/:exerciceId/quiz/:mcqId", { exerciceId: '@idExercice', mcqId: '@id' }, {
+ update: { method: 'PUT' }
+ })
+ })
+ .factory("ExerciceMCQDeps", function ($resource) {
+ return $resource("api/exercices/:exerciceId/quiz/:mcqId/dependancies", { exerciceId: '@idExercice', mcqId: '@id' })
+ })
+ .factory("ExerciceMCQStats", function ($resource) {
+ return $resource("api/exercices/:exerciceId/quiz/:mcqId/statistics", { exerciceId: '@idExercice', mcqId: '@id' })
+ });
+
+angular.module("FICApp")
+ .filter("countHints", function () {
+ return function (input) {
+ if (input == undefined)
+ return 0;
+ return input.reduce(function (sum, n) { return sum + (n.content || n.file) ? 1 : 0; }, 0);
+ }
+ })
+ .filter("toColor", function () {
+ return function (num) {
+ num >>>= 0;
+ var b = (num & 0xFF).toString(16),
+ g = ((num & 0xFF00) >>> 8).toString(16),
+ r = ((num & 0xFF0000) >>> 16).toString(16),
+ a = ((num & 0xFF000000) >>> 24) / 255;
+ if (r.length <= 1) r = "0" + r;
+ if (g.length <= 1) g = "0" + g;
+ if (b.length <= 1) b = "0" + b;
+ return "#" + r + g + b;
+ }
+ })
+ .filter("cksum", function () {
+ return function (input) {
+ if (input == undefined)
+ return input;
+ var raw = atob(input).toString(16);
+ var hex = '';
+ for (var i = 0; i < raw.length; i++) {
+ var _hex = raw.charCodeAt(i).toString(16)
+ hex += (_hex.length == 2 ? _hex : '0' + _hex);
+ }
+ return hex
+ }
+ })
+
+ .component('dependancy', {
+ bindings: {
+ dep: '=',
+ deleteDep: '=',
+ },
+ controller: function () { },
+ template: `
+
+ Flag {{ $ctrl.dep.label }}
+ QCM {{ $ctrl.dep.title }}
+
+
+ `
+ })
+
+ .directive('color', function () {
+ return {
+ require: 'ngModel',
+ link: function (scope, ele, attr, ctrl) {
+ ctrl.$formatters.unshift(function (num) {
+ num >>>= 0;
+ var b = (num & 0xFF).toString(16),
+ g = ((num & 0xFF00) >>> 8).toString(16),
+ r = ((num & 0xFF0000) >>> 16).toString(16),
+ a = ((num & 0xFF000000) >>> 24) / 255;
+ if (r.length <= 1) r = "0" + r;
+ if (g.length <= 1) g = "0" + g;
+ if (b.length <= 1) b = "0" + b;
+ return "#" + r + g + b;
+ });
+ ctrl.$parsers.unshift(function (viewValue) {
+ var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(viewValue);
+ return result ? (
+ parseInt(result[1], 16) * 256 * 256 +
+ parseInt(result[2], 16) * 256 +
+ parseInt(result[3], 16)
+
+ ) : 0;
+ });
+ }
+ };
+ })
+
+ .directive('integer', function () {
+ return {
+ require: 'ngModel',
+ link: function (scope, ele, attr, ctrl) {
+ ctrl.$parsers.unshift(function (viewValue) {
+ return parseInt(viewValue, 10);
+ });
+ }
+ };
+ })
+
+ .directive('float', function () {
+ return {
+ require: 'ngModel',
+ link: function (scope, ele, attr, ctrl) {
+ ctrl.$parsers.unshift(function (viewValue) {
+ return parseFloat(viewValue, 10);
+ });
+ }
+ };
+ })
+
+ .run(function ($rootScope, $http, $interval, $timeout, Settings, $location) {
+ $rootScope.$location = $location;
+ $rootScope.Utils = {
+ keys: Object.keys
+ };
+ $rootScope.refreshSettings = function () {
+ $http.get("api/settings.json").then(function (response) {
+ response.data.start = new Date(response.data.start);
+ response.data.end = new Date(response.data.end);
+ response.data.generation = new Date(response.data.generation);
+ if ($rootScope.settings && $rootScope.settings.activateTime instanceof Date)
+ response.data.activateTime = $rootScope.settings.activateTime;
+ $rootScope.settings = response.data;
+ $rootScope.recvTime(response);
+ })
+ }
+ $rootScope.refreshSettings();
+ $interval($rootScope.refreshSettings, 10000);
+
+ $rootScope.toasts = [];
+ $rootScope.addToast = function (kind, title, msg, yesFunc, noFunc, tmout) {
+ $rootScope.toasts.unshift({
+ variant: kind,
+ title: title,
+ msg: msg,
+ timeout: tmout,
+ yesFunc: yesFunc,
+ noFunc: noFunc,
+ });
+ }
+
+ $rootScope.staticFilesNeedUpdate = 0;
+ $rootScope.staticRegenerationInProgress = false;
+ $rootScope.regenerateStaticFiles = function () {
+ $rootScope.staticRegenerationInProgress = true;
+ $http.post("api/full-generation").then(function (response) {
+ $rootScope.staticFilesNeedUpdate = 0;
+ $rootScope.staticRegenerationInProgress = false;
+ $rootScope.addToast('success', 'Regeneration ended');
+ }, function (response) {
+ $rootScope.staticRegenerationInProgress = false;
+ $rootScope.addToast('error', 'An error occurs when saving settings:', response.data.errmsg);
+ })
+ }
+
+ $rootScope.$on('$locationChangeStart', function (event, next, current) {
+ if ($rootScope.staticFilesNeedUpdate) {
+ $timeout(function () {
+ document.getElementById("circle1").classList.add("play");
+ }, 10);
+ $timeout(function () {
+ document.getElementById("circle1").classList.remove("play");
+ }, 710);
+
+ $timeout(function () {
+ document.getElementById("circle2").classList.add("play");
+ }, 50);
+ $timeout(function () {
+ document.getElementById("circle2").classList.remove("play");
+ }, 750);
+ }
+ });
+
+ $rootScope.logged = parseInt(getCookie("myassignee")) > 0;
+ })
+
+ .controller("VersionController", function ($scope, Version) {
+ $scope.v = Version.get();
+ })
+
+ .controller("TimestampController", function ($scope, $interval, Timestamp) {
+ $scope.t = Timestamp.get();
+ var refresh = function () {
+ $scope.t = Timestamp.get();
+ }
+ var myinterval = $interval(refresh, 2500);
+ $scope.$on('$destroy', function () { $interval.cancel(myinterval); });
+ })
+
+ .controller("HealthController", function ($scope, $interval, Health, $http) {
+ var refresh = function () {
+ $scope.health = Health.query();
+ }
+ refresh();
+ var myinterval = $interval(refresh, 2500);
+ $scope.drop_submission = function (path) {
+ $scope.addToast('info', 'Delete submission', 'Ensure this submission is not interesting. Continue?',
+ function () {
+ $http.delete("api/submissions" + path).then(function (response) {
+ refresh();
+ }, function (response) {
+ $scope.addToast('danger', 'An error occurs when deleting submission:', response.data.errmsg);
+ });
+ }
+ );
+ }
+ $scope.$on('$destroy', function () { $interval.cancel(myinterval); });
+ })
+
+ .controller("MonitorController", function ($scope, Monitor) {
+ $scope.monitor = Monitor.get();
+ })
+
+ .controller("AllTeamAssociationsController", function ($scope, $http) {
+ $scope.newdqa = "";
+ $scope.addDelegatedQA = function () {
+ if ($scope.newdqa.length) {
+ if (!$scope.config.delegated_qa)
+ $scope.config.delegated_qa = [];
+
+ $scope.config.delegated_qa.push($scope.newdqa);
+ $scope.saveSettings();
+ $scope.newdqa = "";
+ }
+ }
+
+ $scope.allAssociations = [];
+ $http.get("api/teams-associations.json").then(function (response) {
+ $scope.allAssociations = response.data;
+ })
+ })
+
+ .controller("SettingsController", function ($scope, $rootScope, NextSettings, Settings, SettingsChallenge, $location, $http, $interval) {
+ $scope.nextsettings = NextSettings.query();
+ $scope.erase = false;
+ $scope.editNextSettings = function (ns) {
+ $scope.activateTime = new Date(ns.date);
+ $rootScope.settings.activateTime = $scope.activateTime;
+ $scope.erase = true;
+ Object.keys(ns.values).forEach(function (k) {
+ $scope.config[k] = ns.values[k];
+ });
+ $scope.config.enableExerciceDepend = $scope.config.unlockedChallengeDepth >= 0;
+ $scope.config.disabledsubmitbutton = $scope.config.disablesubmitbutton && $scope.config.disablesubmitbutton.length > 0;
+ }
+ $scope.deleteNextSettings = function (ns) {
+ ns.$delete().then(function () {
+ $scope.nextsettings = NextSettings.query();
+ })
+ }
+
+ $scope.displayDangerousActions = false;
+ $scope.config = Settings.get();
+ $scope.dist_config = {};
+ $scope.config.$promise.then(function (response) {
+ response.start = new Date(response.start);
+ if (response.end) response.end = new Date(response.end);
+ else response.end = null;
+ response.generation = new Date(response.generation);
+ response.activateTime = new Date(response.activateTime);
+ $scope.dist_config = Object.assign({}, response);
+
+ response.enableExerciceDepend = response.unlockedChallengeDepth >= 0;
+ response.disabledsubmitbutton = response.disablesubmitbutton && response.disablesubmitbutton.length > 0;
+ if (response.end) {
+ $scope.duration = (response.end - response.start)/60000;
+ }
+ })
+ $scope.challenge = SettingsChallenge.get();
+ $scope.duration = 360;
+ $scope.durationChange = function(endChanged) {
+ if (endChanged)
+ $scope.duration = (new Date($scope.config.end).getTime() - new Date($scope.config.start).getTime())/60000;
+ else
+ $scope.config.end = new Date(new Date($scope.config.start).getTime() + $scope.duration * 60000);
+ }
+ $scope.activateTime = "";
+ $scope.challenge.$promise.then(function (c) {
+ if (c.duration)
+ $scope.duration = c.duration;
+ });
+
+ $scope.exerciceDependChange = function () {
+ if ($scope.config.enableExerciceDepend)
+ $scope.config.unlockedChallengeDepth = 0;
+ else
+ $scope.config.unlockedChallengeDepth = -1;
+ };
+
+ $scope.submitButtonStateChange = function () {
+ if ($scope.config.disabledsubmitbutton)
+ $scope.config.disablesubmitbutton = "Mise à jour en cours...";
+ else
+ $scope.config.disablesubmitbutton = "";
+ };
+
+ $scope.dropDelegatedQA = function (member) {
+ if (!$scope.config.delegated_qa) {
+ $scope.config.delegated_qa = [];
+ }
+ angular.forEach($scope.config.delegated_qa, function (m, k) {
+ if (member == m)
+ $scope.config.delegated_qa.splice(k, 1);
+ });
+ $scope.saveSettings();
+ }
+
+ $scope.saveChallengeInfo = function () {
+ this.challenge.duration = $scope.duration;
+ this.challenge.$update(function (response) {
+ $scope.addToast('success', 'Infos du challenge mises à jour avec succès !');
+ }, function (response) {
+ $scope.addToast('danger', 'An error occurs when saving challenge info:', response.data.errmsg);
+ });
+ }
+ $scope.saveSettings = function (msg) {
+ if (msg === undefined) { msg = 'New settings saved!'; }
+ if (this.config.end == "") this.config.end = null;
+
+ var nStart = this.config.start;
+ var nEnd = this.config.end;
+ var nGen = this.config.generation;
+ var state = this.config.enableExerciceDepend;
+ this.config.unlockedChallengeDepth = (this.config.enableExerciceDepend ? this.config.unlockedChallengeDepth : -1)
+ this.config.disablesubmitbutton = (this.config.disabledsubmitbutton ? this.config.disablesubmitbutton : '')
+ var updateQuery = {};
+ if (this.activateTime && this.activateTime != '') {
+ updateQuery['t'] = this.activateTime;
+ this.activateTime = null;
+ }
+ if (this.erase) {
+ updateQuery['erase'] = true;
+ this.erase = false;
+ }
+ this.config.$update(updateQuery, function (response) {
+ $scope.dist_config = Object.assign({}, response);
+ $scope.addToast('success', msg);
+ $scope.nextsettings = NextSettings.query();
+ response.enableExerciceDepend = response.unlockedChallengeDepth >= 0;
+ response.disabledsubmitbutton = response.disablesubmitbutton && response.disablesubmitbutton.length > 0;
+ $rootScope.settings.start = new Date(nStart);
+ if (nEnd) {
+ $rootScope.settings.end = new Date(nEnd);
+ } else {
+ $rootScope.settings.end = null;
+ }
+ $rootScope.settings.generation = new Date(nGen);
+ $scope.updateActivateTime();
+ }, function (response) {
+ $scope.addToast('danger', 'An error occurs when saving settings:', response.data.errmsg);
+ });
+ }
+ $scope.launchChallenge = function () {
+ var ts = $rootScope.getSrvTime().getTime() - $rootScope.getSrvTime().getTime() % 60000;
+ this.config.start = new Date(ts + 120000);
+ this.config.end = new Date(ts + 120000 + this.duration * 60000);
+
+ $scope.addToast('info', 'Challenge ready to start,', 'propagate the changes?',
+ function () {
+ $scope.saveSettings();
+ });
+ }
+ $scope.updateActivateTime = function () {
+ $rootScope.settings.activateTime = this.activateTime;
+ }
+ $scope.updActivateTime = function (modulo) {
+ if (modulo) {
+ var ts = Math.floor((new Date(this.config.end) - $rootScope.getSrvTime().getTime() - (60000 * modulo / 2)) / (60000 * modulo)) * (60000 * modulo);
+ var d = new Date(this.config.end) - ts;
+ this.activateTime = new Date(d);
+ this.updateActivateTime();
+ } else {
+ this.activateTime = null;
+ this.updateActivateTime();
+ }
+ }
+ $scope.reset = function (type) {
+ var txts = {
+ "settings": "En validant, vous remettrez les paramètres de cette page à leur valeur initiale, y compris la date de début du challenge.",
+ "challengeInfo": "En validant, vous effacerez les informations descriptives du challenge.",
+ "challenges": "En validant, vous retirerez toutes les données statiques des challenges.",
+ "teams": "En validant, vous supprimerez l'ensemble des équipes enregistreées.",
+ "game": "En validant, vous supprimerez toutes les tentatives, les validations, ... faites par les équipes.",
+ }
+ $scope.addToast('warning', txts[type], 'Êtes-vous sûr de vouloir continuer ?',
+ function () {
+ if (confirm("Êtes-vous vraiment sûr ?\n" + txts[type])) {
+ $http.post("api/reset", { "type": type }).then(function (time) {
+ $scope.addToast('success', type + 'reseted');
+ $location.url("/");
+ }, function (response) {
+ $scope.addToast('danger', 'An error occurs when reseting ' + type + ':', response.data.errmsg);
+ });
+
+ }
+ });
+ };
+ $scope.switchToProd = function () {
+ $scope.addToast('warning', "Activer le mode challenge ?", "L'activation du mode challenge est temporaire (vous devriez plutôt relancer le daemon avec l'option `-4real`). Ce mode permet d'éviter les mauvaises manipulations et désactive le hook git de synchronisation automatique. Êtes-vous sûr de vouloir continuer ?",
+ function () {
+ $http.put("api/prod", true).then(function (time) {
+ $rootScope.refreshSettings()
+ $scope.addToast('success', 'Mode challenge activé');
+ }, function (response) {
+ $scope.addToast('danger', 'An error occurs when activating challenge mode:', response.data.errmsg);
+ });
+ });
+ };
+ })
+
+ .controller("RepositoriesController", function ($scope, $http) {
+ $http.get("api/repositories").then(function (response) {
+ $scope.repositories = response.data.repositories;
+ });
+
+ $scope.deleteRepository = function(repo) {
+ $http.delete("api/repositories/" + repo.path).then(function (response) {
+ $scope.repositories[$scope.repositories.indexOf(repo)].hash = "- DELETED -";
+ });
+ };
+ })
+ .component('teamLink', {
+ bindings: {
+ idTeam: '=',
+ },
+ controller: function (Team) {
+ var ctrl = this;
+ ctrl.team = {};
+
+ ctrl.$onInit = function () {
+ ctrl.team = Team.get({teamId: ctrl.idTeam});
+ };
+ },
+ template: `{{ $ctrl.team.name }} `
+ })
+ .component('repositoryUptodate', {
+ bindings: {
+ repository: '<',
+ },
+ controller: function ($http) {
+ var ctrl = this;
+
+ ctrl.status = {};
+ ctrl.color = "badge-secondary";
+
+ ctrl.$onInit = function () {
+ $http.post("api/repositories/" + ctrl.repository.path).then(function (response) {
+ ctrl.status = response.data;
+
+ if (ctrl.repository.hash.startsWith(ctrl.status.hash)) {
+ ctrl.color = "badge-success";
+ } else {
+ ctrl.color = "badge-danger";
+ }
+ });
+ };
+ },
+ template: `{{ $ctrl.status.hash }} {{ $ctrl.status.text }} `
+ })
+
+ .controller("SyncController", function ($scope, $rootScope, $location, $http, $interval) {
+ $scope.displayDangerousActions = false;
+
+ var needRefreshSyncReportWhenReady = false;
+ var refreshSyncReport = function () {
+ needRefreshSyncReportWhenReady = false;
+ $http.get("full_import_report.json").then(function (response) {
+ $scope.syncReport = response.data;
+ })
+ };
+ refreshSyncReport()
+
+ $scope.deepSyncInProgress = false;
+
+ var progressInterval = $interval(function () {
+ $http.get("api/sync/status").then(function (response) {
+ if (response.data.progress && response.data.progress != 255)
+ needRefreshSyncReportWhenReady = true;
+ else if (needRefreshSyncReportWhenReady)
+ refreshSyncReport();
+ if (response.data && response.data.progress) {
+ $scope.syncPercent = Math.floor(response.data.progress * 100 / 255);
+ $scope.deepSyncInProgress = response.data.pullMutex && response.data.syncMutex;
+ } else {
+ $scope.syncPercent = 0;
+ }
+ $scope.syncStatus = response.data;
+ }, function (response) {
+ $scope.syncPercent = 0;
+ $scope.syncStatus = response.data;
+ })
+ }, 1500);
+ $scope.$on('$destroy', function () { $interval.cancel(progressInterval); });
+
+ $scope.deepSync = function (theme) {
+ if (theme) {
+ question = 'Faire une synchronisation intégrale du thème ' + theme.name + ' ?'
+ url = "api/sync/deep/" + theme.id
+ } else {
+ question = 'Faire une synchronisation intégrale ?'
+ url = "api/sync/deep"
+ }
+ $scope.addToast('warning', question, '',
+ function () {
+ $scope.deepSyncInProgress = true;
+ $http.post(url).then(function () {
+ $scope.deepSyncInProgress = false;
+ $scope.addToast('success', 'Synchronisation intégrale terminée.', 'Voir le rapport .', null, null, 15000);
+ }, function (response) {
+ $scope.deepSyncInProgress = false;
+ $scope.addToast('warning', 'Synchronisation intégrale terminée.', 'Voir le rapport .', null, null, 15000);
+ });
+ });
+ };
+ $scope.speedyDeepSync = function () {
+ $scope.addToast('warning', 'Faire une synchronisation profonde rapide, sans s\'occuper des fichiers ?', '',
+ function () {
+ $scope.deepSyncInProgress = true;
+ $http.post("api/sync/speed").then(function () {
+ $scope.deepSyncInProgress = false;
+ $scope.addToast('success', 'Synchronisation profonde rapide terminée.', 'Voir le rapport .', null, null, 15000);
+ }, function (response) {
+ $scope.deepSyncInProgress = false;
+ $scope.addToast('warning', 'Synchronisation profinde rapide terminée.', 'Voir le rapport .', null, null, 15000);
+ });
+ });
+ };
+ $scope.baseSync = function () {
+ $scope.addToast('warning', 'Tirer les mises à jour du dépôt ?', '',
+ function () {
+ $scope.deepSyncInProgress = true;
+ $http.post("api/sync/base").then(function () {
+ $scope.deepSyncInProgress = false;
+ $scope.addToast('success', 'Mise à jour terminée.');
+ }, function (response) {
+ $scope.deepSyncInProgress = false;
+ $scope.addToast('danger', 'Mise à jour terminée.', response.data.errmsg);
+ });
+ });
+ };
+ $scope.syncVideos = function () {
+ $scope.addToast('warning', 'Synchroniser les vidéos de résolution ?', 'ATTENTION il ne faut pas lancer cette synchronisation durant le challenge. Seulement une fois le challenge terminé, cela permet de rendre les vidéos accessibles dans l\'interface joueurs.',
+ function () {
+ $scope.deepSyncInProgress = true;
+ $http.post("api/sync/videos").then(function () {
+ $scope.deepSyncInProgress = false;
+ $scope.addToast('success', 'Import des vidéos terminé.');
+ }, function (response) {
+ $scope.deepSyncInProgress = false;
+ $scope.addToast('danger', 'Import des vidéos terminé.', response.data.errmsg);
+ });
+ });
+ };
+ $scope.dropSoluces = function () {
+ $scope.addToast('warning', 'Effacer les solutions', 'Ceci va retirer les textes de résolution de la base de données ainsi que les liens vers les vidéos.',
+ function () {
+ $scope.deepSyncInProgress = true;
+ $http.post("api/sync/drop_soluces").then(function () {
+ $scope.deepSyncInProgress = false;
+ $scope.addToast('success', 'Effacement des solutions terminé.');
+ }, function (response) {
+ $scope.deepSyncInProgress = false;
+ $scope.addToast('danger', 'Effacement des solutions terminé avec des erreurs.', response.data.errmsg);
+ });
+ });
+ };
+ $scope.diffWithRepo = function () {
+ $scope.diff = null;
+ $http({
+ url: "api/sync/local-diff",
+ method: "POST"
+ }).then(function (response) {
+ $scope.diff = response.data;
+ if (response.data === null) {
+ $scope.addToast('success', 'Changements par rapport au dépôt', "Tout est pareil !");
+ }
+ }, function (response) {
+ $scope.diff = null;
+ $scope.addToast('danger', 'An error occurs when synchronizing exercice:', response.data.errmsg);
+ });
+ };
+ })
+
+ .controller("AuthController", function ($scope, $http) {
+ $scope.generateHtpasswd = function () {
+ $http.post("api/htpasswd").then(function () {
+ $scope.addToast('success', 'Fichier htpasswd généré avec succès');
+ }, function (response) {
+ $scope.addToast('danger', 'An error occurs when generating htpasswd file:', response.data.errmsg);
+ });
+ };
+ $scope.removeHtpasswd = function () {
+ $http.delete("api/htpasswd").then(function () {
+ $scope.addToast('success', 'Fichier htpasswd supprimé avec succès');
+ }, function (response) {
+ $scope.addToast('danger', 'An error occurs when deleting htpasswd file:', response.data.errmsg);
+ });
+ };
+ })
+
+ .controller("OAuthController", function ($scope, $http) {
+ $scope.oauth_status = {};
+ $scope.refreshOAuthStatus = function () {
+ $http.get("api/oauth-status").then(function (res) {
+ $scope.oauth_status = res.data;
+ });
+ };
+ $scope.refreshOAuthStatus();
+
+ $scope.genDexCfg = function () {
+ $http.post("api/dex.yaml").then(function () {
+ $http.post("api/dex-password.tpl").then(function () {
+ $scope.addToast('success', 'Dex config refreshed.', "Don't forget to reload/reboot frontend host.");
+ }, function (response) {
+ $scope.addToast('danger', 'An error occurs when generating dex password tpl:', response.data.errmsg);
+ });
+ }, function (response) {
+ $scope.addToast('danger', 'An error occurs when generating dex config:', response.data.errmsg);
+ });
+ $http.post("api/vouch-proxy.yaml").then(function () {
+ $scope.addToast('success', 'VouchProxy config refreshed.', "Don't forget to reload/reboot frontend host.");
+ }, function (response) {
+ $scope.addToast('danger', 'An error occurs when generating VouchProxy config:', response.data.errmsg);
+ });
+ }
+ })
+
+ .controller("PKIController", function ($scope, $rootScope, Certificate, CACertificate, Team, $location, $http) {
+ var ts = Date.now() - Date.now() % 86400000;
+ var d = new Date(ts);
+ var f = new Date(ts + 3 * 86400000);
+ $scope.newca = {
+ notAfter: f.toISOString(),
+ notBefore: d.toISOString(),
+ };
+
+ $scope.teams = Team.query();
+ $scope.certificates = Certificate.query();
+ $scope.ca = CACertificate.get();
+
+ $scope.revoke = function () {
+ var targetserial = $("#revokeModal").data("certificate");
+ if (targetserial) {
+ Certificate.delete({ serial: targetserial }).$promise.then(
+ function () {
+ $('#revokeModal').modal('hide');
+ $scope.certificates = Certificate.query().$promise.then(function (certificates) {
+ certificates.forEach(function (certificate, cid) {
+ certificate.serial = parseInt(certificate.id).toString(16);
+ });
+ });
+ }, function (response) {
+ $scope.addToast('danger', 'An error occurs when trying to associate certificate:', response.data.errmsg);
+ }
+ );
+ }
+ };
+
+ $scope.validateSearch = function (keyEvent) {
+ if (keyEvent.which === 13) {
+ var myCertificate = null;
+ $scope.certificates.forEach(function (certificate) {
+ if (String(certificate.id).indexOf($scope.query.toUpperCase()) >= 0) {
+ if (myCertificate === null)
+ myCertificate = certificate;
+ else
+ myCertificate = false;
+ }
+ });
+ if (myCertificate && myCertificate.id_team == null) {
+ $('#associationModal').data('certificate', myCertificate.id)
+ $('#associationModal').modal()
+ }
+ }
+ };
+ $scope.validatePKIForm = function (keyEvent) {
+ if (keyEvent.which === 13)
+ $scope.associate()
+ };
+ $scope.associate = function () {
+ var targetserial = $("#associationModal").data("certificate");
+ if (!targetserial) return;
+ Certificate.update({ serial: targetserial }, { id_team: $scope.selectedTeam }).$promise.then(
+ function () {
+ $('#associationModal').modal('hide');
+ $scope.certificates = Certificate.query();
+ $scope.selectedTeam = null;
+ }, function (response) {
+ $scope.addToast('danger', 'An error occurs when trying to associate certificate:', response.data.errmsg);
+ }
+ );
+ };
+
+ $scope.generateCA = function () {
+ $http.post("api/ca/new", $scope.newca).then(function () {
+ $scope.ca = CACertificate.get();
+ }, function (response) {
+ $scope.addToast('danger', 'An error occurs when generating CA:', response.data.errmsg);
+ });
+ };
+ $scope.renewCA = function () {
+ $scope.ca = {};
+ };
+
+ $scope.generateCert = function () {
+ $http.post("api/certs").then(function () {
+ $scope.certificates = Certificate.query();
+ }, function (response) {
+ $scope.addToast('danger', 'An error occurs when generating certificate:', response.data.errmsg);
+ });
+ };
+ })
+
+ .controller("PublicController", function ($scope, $rootScope, $routeParams, $location, Scene, Theme, Teams, Exercice) {
+ $scope.propagationtime = null;
+ $scope.presetName = "";
+ $scope.screens = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
+ $scope.screenid = $routeParams.screenId;
+ $scope.display = Scene.get({ screenId: $routeParams.screenId });
+ $scope.listScenes = Scene.get();
+ $scope.themes = Theme.query();
+ $scope.teams = Teams.get();
+
+ $scope.chScreen = function (sid) {
+ if ($scope.screenid)
+ $location.url("/public/" + $scope.screenid);
+ else
+ $location.url("/public/");
+ }
+
+ $scope.types = {
+ "welcome": "Messages de bienvenue",
+ "countdown": "Compte à rebours",
+ "message": "Message",
+ "panel": "Boîte",
+ "carousel": "Carousel",
+ "exercice": "Exercice",
+ "table": "Tableau",
+ "rank": "Classement",
+ "graph": "Graphique",
+ };
+ $scope.typeside = {
+ "welcome": "Messages de bienvenue",
+ "themes": "Présentation des thèmes",
+ "exercice_follow": "Dernier exercice des événements",
+ "exercice": "Exercice",
+ "rank": "Classement",
+ "graph": "Graphique",
+ "message": "Message",
+ "panel": "Boîte",
+ };
+ $scope.welcome_types = {
+ "teams": "Accueil des équipes",
+ "public": "Accueil du public",
+ };
+ $scope.carousel_types = {
+ "exercices": "Exercices",
+ "teams": "Équipes",
+ "themes": "Thèmes",
+ "ranking": "Classement",
+ };
+ $scope.colors = {
+ "primary": "Primaire",
+ "secondary": "Secondaire",
+ "info": "Info",
+ "success": "Success",
+ "warning": "Warning",
+ "danger": "Danger",
+ "light": "Clair",
+ "dark": "Foncé",
+ };
+ $scope.rank_types = {
+ "general": "Classement général",
+ "final": "Classement final",
+ };
+ $scope.rank_types_side = {
+ "carousel": "Classement général carousel",
+ "general": "Classement général",
+ };
+ $scope.table_types = {
+ "levels": "Niveaux d'exercices",
+ "teams": "Équipes",
+ };
+ $scope.exercices = Exercice.query();
+
+ $scope.clearScene = function () {
+ $scope.someUpdt = true;
+ $scope.display.scenes = [];
+ };
+ $scope.presetScene = function (scene) {
+ $scope.someUpdt = true;
+ if (scene == "registration") {
+ $scope.display.scenes = [
+ {
+ type: "welcome",
+ params: { kind: "teams", url: "https://fic.srs.epita.fr/" },
+ },
+ {
+ type: "welcome",
+ params: { kind: "public", notitle: true },
+ },
+ ];
+ $scope.display.side = [
+ {
+ type: "themes",
+ params: {},
+ },
+ ];
+ }
+ else if (scene == "welcome") {
+ $scope.display.scenes = [
+ {
+ type: "carousel",
+ params: { color: "info", kind: "themes", title: "Présentation des entreprises ciblées" },
+ },
+ ];
+ $scope.display.side = [
+ {
+ type: "welcome",
+ params: { kind: "public" },
+ },
+ ];
+ }
+ else if (scene == "start") {
+ $scope.display.scenes = [
+ {
+ type: "welcome",
+ params: { kind: "public" },
+ },
+ {
+ type: "countdown",
+ params: { color: "success", end: null, lead: "Go, go, go !", title: "Le challenge forensic va bientôt commencer !" },
+ },
+ ];
+ $scope.display.side = [
+ {
+ type: "themes",
+ params: {},
+ },
+ ];
+ }
+ else if (scene == "end") {
+ $scope.display.scenes = [
+ {
+ type: "rank",
+ params: { which: "final" },
+ },
+ {
+ type: "table",
+ params: { kind: "teams", themes: $scope.themes.map(function (z, i) { return z.id; }), total: true, teams: [] },
+ },
+ ];
+ angular.forEach($scope.teams, function (team, tid) {
+ if (team.rank >= 1 && team.rank <= 4)
+ $scope.display.scenes[1].params.teams.push(tid)
+ });
+ $scope.display.side = [
+ {
+ type: "rank",
+ params: { which: "carousel" },
+ },
+ {
+ type: "graph",
+ params: { teams: [], height: 400, legend: false, hide: true },
+ },
+ {
+ type: "message",
+ params: { html: '', hide: true },
+ },
+ ];
+ angular.forEach($scope.teams, function (team, tid) {
+ if (team.rank >= 1 && team.rank <= 9)
+ $scope.display.side[1].params.teams.push(tid)
+ });
+ $scope.display.hideEvents = true;
+ $scope.display.hideCountdown = true;
+ }
+ else if (scene == "summary") {
+ $scope.display.scenes = [
+ {
+ type: "table",
+ params: { kind: "levels", levels: [1, 2, 3, 4, 5], themes: $scope.themes.map(function (z, i) { return z.id; }), total: true },
+ },
+ {
+ type: "graph",
+ params: { teams: [], height: 337, legend: true },
+ },
+ ];
+ angular.forEach($scope.teams, function (team, tid) {
+ if (team.rank >= 1 && team.rank <= 9)
+ $scope.display.scenes[1].params.teams.push(tid)
+ });
+ $scope.display.side = [
+ {
+ type: "exercice_follow",
+ params: {},
+ },
+ ];
+ $scope.display.hideEvents = false;
+ $scope.display.hideCountdown = false;
+ }
+ else if (scene == "summary2") {
+ $scope.display.scenes = [
+ {
+ type: "graph",
+ params: { teams: [], height: 400, legend: false },
+ },
+ {
+ type: "rank",
+ params: { limit: 10, which: "general", legend: true },
+ },
+ ];
+ angular.forEach($scope.teams, function (team, tid) {
+ if (team.rank >= 1 && team.rank <= 9)
+ $scope.display.scenes[0].params.teams.push(tid)
+ });
+ $scope.display.side = [
+ {
+ type: "exercice_follow",
+ params: {},
+ },
+ ];
+ $scope.display.hideEvents = false;
+ $scope.display.hideCountdown = false;
+ }
+ else if (scene == "summary3") {
+ $scope.display.scenes = [
+ {
+ type: "table",
+ params: { kind: "levels", levels: [1, 2, 3, 4, 5], themes: $scope.themes.map(function (z, i) { return z.id; }), total: true },
+ },
+ {
+ type: "rank",
+ params: { limit: 10, which: "general", legend: false },
+ },
+ ];
+ $scope.display.side = [
+ {
+ type: "exercice_follow",
+ params: {},
+ },
+ ];
+ $scope.display.hideEvents = false;
+ $scope.display.hideCountdown = false;
+ }
+ else if (scene == "happyhour") {
+ $scope.display.customCountdown = {
+ show: true,
+ shadow: "#E8CF5C",
+ end: new Date($rootScope.getSrvTime().getTime() + 1802000).toISOString(),
+ before: "Heure joyeuse : chaque résolution compte double !",
+ after: "Heure joyeuse terminée !",
+ }
+ }
+ else if (scene == "freehintquarter") {
+ $scope.display.customCountdown = {
+ show: true,
+ shadow: "#3DD28F",
+ end: new Date($rootScope.getSrvTime().getTime() + 902000).toISOString(),
+ before: "Quart d'heure facile : indices dévoilés !",
+ after: "Quart d'heure facile terminée !",
+ }
+ }
+ };
+
+ $scope.genSceneCountdownDate = function (scene, duration) {
+ scene.params.end = (new Date($rootScope.getSrvTime().getTime() + duration)).toISOString();
+ }
+ $scope.genCustomCountdownDate = function (duration) {
+ if (duration == null) {
+ $scope.display.customCountdown.end = $rootScope.settings.activateTime;
+ } else {
+ $scope.display.customCountdown.end = (new Date($rootScope.getSrvTime().getTime() + duration)).toISOString();
+ }
+ }
+
+ $scope.loadFile = function (fname) {
+ $scope.display = Scene.get({ screenId: fname });
+ $scope.someUpdt = true;
+ };
+ $scope.deleteFile = function (fname) {
+ Scene.delete({ screenId: fname });
+ $scope.listScenes = Scene.get();
+ };
+ $scope.latePropagation = function () {
+ $scope.someUpdt = false;
+ var prms = Scene.update({ screenId: $scope.screenid, t: $scope.propagationTime }, $scope.display);
+ prms.$promise.then(function () {
+ $scope.addToast('success', 'Scene successfully planned!');
+ }, function (response) {
+ $scope.addToast('danger', 'An error occurs when planning scene:', response.data.errmsg);
+ });
+ };
+ $scope.savePreset = function () {
+ $scope.someUpdt = false;
+ var prms = Scene.update({ screenId: $scope.screenid, p: $scope.presetName }, $scope.display);
+ prms.$promise.then(function () {
+ $scope.addToast('success', 'Preset successfully saved!');
+ }, function (response) {
+ $scope.addToast('danger', 'An error occurs when saving preset:', response.data.errmsg);
+ });
+ };
+ $scope.saveScenes = function () {
+ $scope.someUpdt = false;
+ var prms = Scene.update({ screenId: $scope.screenid }, $scope.display);
+ prms.$promise.then(function () {
+ $scope.addToast('success', 'Scene successfully published!');
+ }, function (response) {
+ $scope.addToast('danger', 'An error occurs when saving scene:', response.data.errmsg);
+ });
+ };
+ $scope.addSide = function () {
+ $scope.someUpdt = true;
+ $scope.display.side.push({ params: {} });
+ };
+ $scope.delSide = function (s) {
+ $scope.someUpdt = true;
+ angular.forEach($scope.display.side, function (scene, k) {
+ if (scene == s)
+ $scope.display.side.splice(k, 1);
+ });
+ };
+ $scope.upSide = function (s) {
+ $scope.someUpdt = true;
+ angular.forEach($scope.display.side, function (scene, k) {
+ if (scene == s && k > 0) {
+ $scope.display.side.splice(k, 1);
+ $scope.display.side.splice(k - 1, 0, scene);
+ }
+ });
+ };
+ $scope.downSide = function (s) {
+ $scope.someUpdt = true;
+ var move = true;
+ angular.forEach($scope.display.side, function (scene, k) {
+ if (move && scene == s) {
+ $scope.display.side.splice(k, 1);
+ $scope.display.side.splice(k + 1, 0, scene);
+ move = false;
+ }
+ });
+ };
+ $scope.addScene = function () {
+ $scope.someUpdt = true;
+ $scope.display.scenes.push({ params: {} });
+ };
+ $scope.delScene = function (s) {
+ $scope.someUpdt = true;
+ angular.forEach($scope.display.scenes, function (scene, k) {
+ if (scene == s)
+ $scope.display.scenes.splice(k, 1);
+ });
+ };
+ $scope.upScene = function (s) {
+ $scope.someUpdt = true;
+ angular.forEach($scope.display.scenes, function (scene, k) {
+ if (scene == s && k > 0) {
+ $scope.display.scenes.splice(k, 1);
+ $scope.display.scenes.splice(k - 1, 0, scene);
+ }
+ });
+ };
+ $scope.downScene = function (s) {
+ $scope.someUpdt = true;
+ var move = true;
+ angular.forEach($scope.display.scenes, function (scene, k) {
+ if (move && scene == s) {
+ $scope.display.scenes.splice(k, 1);
+ $scope.display.scenes.splice(k + 1, 0, scene);
+ move = false;
+ }
+ });
+ };
+ })
+
+ .controller("FilesListController", function ($scope, File, $location, $http, $rootScope) {
+ $scope.files = File.query();
+ $scope.errfnd = null;
+ $scope.errzip = null;
+ $scope.clearFilesWIP = false;
+ $scope.fields = ["id", "path", "name", "size"];
+
+ $scope.clearFiles = function (id) {
+ File.delete(function () {
+ $rootScope.staticFilesNeedUpdate++;
+ $scope.files = [];
+ });
+ };
+ $scope.clearFilesDir = function () {
+ $scope.addToast('warning', 'Êtes-vous sûr de vouloir continuer ?', "Ceci va supprimer tout le contenu du dossier FILES. Il s'agit des fichiers ci-dessous, il faudra refaire une synchronisation ensuite.",
+ function () {
+ $scope.clearFilesWIP = true;
+ $http({
+ url: "api/files",
+ method: "DELETE"
+ }).then(function (response) {
+ $scope.clearFilesWIP = false;
+ }, function (response) {
+ $scope.clearFilesWIP = false;
+ $scope.addToast('danger', 'An error occurs when trying to clear files:', response.data.errmsg);
+ });
+ });
+ };
+ $scope.gunzipFile = function (f) {
+ f.gunzipWIP = true;
+ $http({
+ url: "api/files/" + f.id + "/gunzip",
+ method: "POST"
+ }).then(function (response) {
+ f.gunzipWIP = false;
+ f.err = true;
+ }, function (response) {
+ f.gunzipWIP = false;
+ $scope.inSync = false;
+ $scope.errzip += 1;
+ f.err = response.data.errmsg;
+ })
+ };
+ $scope.checksum = function (f) {
+ f.checkWIP = true;
+ $http({
+ url: "api/files/" + f.id + "/check",
+ method: "POST"
+ }).then(function (response) {
+ f.checkWIP = false;
+ f.err = true;
+ }, function (response) {
+ f.checkWIP = false;
+ $scope.inSync = false;
+ $scope.errfnd += 1;
+ f.err = response.data.errmsg;
+ })
+ };
+ $scope.checksumAll = function () {
+ $scope.errfnd = null;
+ angular.forEach($scope.files, function (file) {
+ $scope.checksum(file);
+ });
+ if ($scope.errfnd === null) $scope.errfnd = 0;
+ };
+ $scope.gunzipFiles = function () {
+ $scope.errzip = null;
+ angular.forEach($scope.files, function (file) {
+ $scope.gunzipFile(file);
+ });
+ if ($scope.errzip === null) $scope.errzip = 0;
+ };
+ $scope.show = function (f) {
+ $location.url("/exercices/" + f.idExercice);
+ };
+ })
+
+ .controller("EventsListController", function ($scope, Event, $location) {
+ $scope.events = Event.query();
+ $scope.fields = ["id", "kind", "txt", "time"];
+
+ $scope.clearEvents = function (id) {
+ Event.delete(function () {
+ $scope.events = [];
+ });
+ };
+ $scope.show = function (id) {
+ $location.url("/events/" + id);
+ };
+ })
+ .controller("EventController", function ($scope, Event, $routeParams, $location) {
+ $scope.event = Event.get({ eventId: $routeParams.eventId });
+ $scope.fields = ["kind", "txt", "time"];
+ $scope.kinds = {
+ "secondary": "Par défaut",
+ "primary": "Mise en valeur",
+ "info": "Info",
+ "warning": "Warning",
+ "success": "Success",
+ "danger": "Danger",
+ "light": "Clair",
+ "dark": "Foncé",
+ };
+
+ $scope.saveEvent = function () {
+ if (this.event.id) {
+ this.event.$update();
+ } else {
+ this.event.$save(function () {
+ $location.url("/events/" + $scope.event.id);
+ });
+ }
+ }
+ $scope.deleteEvent = function () {
+ this.event.$remove(function () { $location.url("/events/"); });
+ }
+ })
+
+ .controller("AssigneesListController", function ($scope, ClaimAssignee, $location) {
+ $scope.assignees = ClaimAssignee.query();
+
+ $scope.setMyAId = function (aid) {
+ setCookie("myassignee", aid, 5);
+ $location.url("/claims/");
+ }
+ $scope.whoami = getCookie("myassignee");
+ $scope.newAssignee = function () {
+ $scope.assignees.push(new ClaimAssignee());
+ }
+ $scope.edit = function (a) {
+ a.edit = true;
+ }
+ $scope.updateAssignee = function (a) {
+ if (a.id) {
+ a.$update(function () { $location.url("/claims/"); });
+ } else {
+ a.$save()
+ }
+ }
+ $scope.removeAssignee = function (a) {
+ a.$remove(function () { $location.url("/claims/"); });
+ }
+ })
+ .controller("ClaimsTinyListController", function ($scope, Claim, ClaimAssignee, $interval) {
+ $scope.whoami = getCookie("myassignee");
+
+ var priorities = {
+ "low": 1,
+ "medium": 2,
+ "high": 3,
+ "critical": 4,
+ };
+ $scope.priorities = [
+ "secondary",
+ "light",
+ "info",
+ "warning",
+ "danger",
+ ];
+
+ var refresh = function () {
+ Claim.query().$promise.then(function (claims) {
+ $scope.newClaims = 0;
+ $scope.newClaimsMaxLevel = 0;
+ $scope.myClaims = 0;
+ $scope.myClaimsMaxLevel = 0;
+
+ claims.forEach(function (claim, cid) {
+ if ($scope.whoami && !claim.id_assignee && claim.state == 'new') {
+ $scope.newClaims++;
+ if (priorities[claim.priority] > $scope.newClaimsMaxLevel)
+ $scope.newClaimsMaxLevel = priorities[claim.priority];
+ }
+ else if ($scope.whoami && claim.id_assignee == $scope.whoami && claim.state != 'closed' && claim.state != 'invalid') {
+ $scope.myClaims++;
+ if (claim.state == 'new' && priorities[claim.priority] > $scope.myClaimsMaxLevel)
+ $scope.myClaimsMaxLevel = priorities[claim.priority];
+ }
+ })
+ });
+ };
+ refresh();
+ $interval(refresh, 10000);
+ })
+ .controller("ClaimsListController", function ($scope, Claim, ClaimAssignee, Teams, $interval, $location) {
+ var refresh = function () {
+ $scope.claims = Claim.query();
+ $scope.assignees = ClaimAssignee.query();
+ }
+ refresh();
+ var myinterval = $interval(refresh, 10000);
+ $scope.$on('$destroy', function () { $interval.cancel(myinterval); });
+
+ $scope.whoami = getCookie("myassignee");
+ $scope.teams = Teams.get();
+ $scope.fields = ["subject", "id_team", "state", "id_assignee", "last_update", "id"];
+
+ $scope.order = "priority";
+ $scope.chOrder = function (no) {
+ $scope.order = no;
+ };
+
+ $scope.clearClaims = function (id) {
+ Claim.delete(function () {
+ $scope.claims = [];
+ });
+ };
+ $scope.show = function (id) {
+ $location.url("/claims/" + id);
+ };
+ })
+ .controller("ClaimLastUpdateController", function ($scope, $http) {
+ $scope.init = function (claim) {
+ $http.get("api/claims/" + claim.id + "/last_update").then(function (response) {
+ if (response.data)
+ $scope.last_update = response.data;
+ else
+ $scope.last_update = claim.creation;
+ claim.last_update = $scope.last_update;
+ }, function (response) {
+ $scope.last_update = claim.creation;
+ })
+ }
+ })
+ .controller("ClaimController", function ($scope, Claim, ClaimAssignee, Teams, Exercice, $routeParams, $location, $http, $rootScope) {
+ $scope.claim = Claim.get({ claimId: $routeParams.claimId }, function (v) {
+ v.id_team = "" + v.id_team;
+ if (!v.priority)
+ v.priority = "medium";
+ });
+ if ($routeParams.claimId == "new")
+ $scope.fields = ["id_team", "id_exercice", "subject", "priority", "id_assignee"];
+ else
+ $scope.fields = ["subject", "priority", "id_exercice", "id_assignee", "id_team", "creation", "state"];
+ $scope.assignees = ClaimAssignee.query();
+ $scope.comm = { ndescription: "" };
+ $scope.whoami = Math.floor(getCookie("myassignee"));
+ $scope.teams = Teams.get();
+ $scope.exercices = Exercice.query();
+ $scope.namedFields = {
+ "subject": "Objet",
+ "id_assignee": "Assigné à",
+ "state": "État",
+ "id_team": "Équipe",
+ "id_exercice": "Challenge",
+ "creation": "Création",
+ "priority": "Priorité",
+ "description": "Description",
+ };
+ $scope.states = {
+ "new": "Nouveau",
+ "need-info": "Besoin d'infos",
+ "confirmed": "Confirmé",
+ "in-progress": "En cours",
+ "need-review": "Fait",
+ "closed": "Clos",
+ "invalid": "Invalide",
+ };
+ $scope.priorities = {
+ "low": "Basse",
+ "medium": "Moyenne",
+ "high": "Haute",
+ "critical": "Critique",
+ };
+
+ $scope.changeState = function (state) {
+ this.claim.state = state;
+ if ((state == "in-progress" || state == "invalid") && this.claim.id_assignee)
+ this.claim.id_assignee = $scope.whoami;
+ if (this.claim.id)
+ this.saveClaim(state == "invalid" || state == "closed");
+ }
+ $scope.assignToMe = function () {
+ this.claim.id_assignee = $scope.whoami;
+ if (this.claim.id)
+ this.saveClaim(false);
+ }
+ $scope.updateDescription = function (description) {
+ $http({
+ url: "api/claims/" + $scope.claim.id + "/descriptions",
+ method: "PUT",
+ data: description
+ }).then(function (response) {
+ $scope.claim = Claim.get({ claimId: $routeParams.claimId }, function (v) {
+ v.id_team = "" + v.id_team;
+ if (!v.priority)
+ v.priority = "medium";
+ });
+ });
+ }
+ $scope.saveDescription = function () {
+ $http({
+ url: "api/claims/" + $scope.claim.id,
+ method: "POST",
+ data: {
+ "id_assignee": $scope.whoami,
+ "content": $scope.comm.ndescription
+ }
+ }).then(function (response) {
+ $location.url("/claims/" + $scope.claim.id + "/");
+ });
+ }
+ $scope.saveClaim = function (backToList) {
+ this.claim.whoami = $scope.whoami;
+ if (this.claim.id_team) {
+ this.claim.id_team = parseInt(this.claim.id_team, 10);
+ } else {
+ this.claim.id_team = null;
+ }
+ if (this.claim.id) {
+ this.claim.$update(function (v) {
+ v.id_team = "" + v.id_team;
+ if ($scope.comm.ndescription)
+ $scope.saveDescription();
+ else if (backToList)
+ $location.url("/claims/");
+ else
+ $scope.claim = Claim.get({ claimId: $routeParams.claimId }, function (v) {
+ v.id_team = "" + v.id_team;
+ if (!v.priority)
+ v.priority = "medium";
+ });
+ });
+ } else {
+ this.claim.$save(function () {
+ if (!$scope.comm.ndescription)
+ $scope.comm.ndescription = "Création de la tâche";
+ $scope.saveDescription();
+ }, function (response) {
+ $scope.addToast('danger', 'An error occurs when trying to save claim:', response.data.errmsg);
+ });
+ }
+ }
+ $scope.deleteClaim = function () {
+ this.claim.$remove(function () { $location.url("/claims/"); });
+ }
+ })
+
+ .controller("ThemesListController", function ($scope, Theme, $location, $rootScope, $http) {
+ $scope.themes = Theme.query();
+ $scope.fields = ["name", "authors", "headline", "path"];
+
+ $scope.validateSearch = function (keyEvent) {
+ if (keyEvent.which === 13) {
+ var myTheme = null;
+ $scope.themes.forEach(function (theme) {
+ if (String(theme.name.toLowerCase()).indexOf($scope.query.toLowerCase()) >= 0) {
+ if (myTheme === null)
+ myTheme = theme;
+ else
+ myTheme = false;
+ }
+ });
+ if (myTheme)
+ $location.url("themes/" + myTheme.id);
+ }
+ };
+
+ $scope.show = function (id) {
+ $location.url("/themes/" + id);
+ };
+ $scope.inSync = false;
+ $scope.sync = function () {
+ $scope.inSync = true;
+ $http({
+ url: "api/sync/themes",
+ method: "POST"
+ }).then(function (response) {
+ $scope.inSync = false;
+ $scope.themes = Theme.query();
+ $rootScope.staticFilesNeedUpdate++;
+ if (response.data)
+ $scope.addToast('danger', 'An error occurs when synchronizing theme list:', response.data);
+ else
+ $scope.addToast('success', 'Synchronisation de la liste des thèmes terminée avec succès.');
+ }, function (response) {
+ $scope.inSync = false;
+ $scope.addToast('danger', 'An error occurs when synchronizing theme list:', response.data.errmsg);
+ });
+ };
+ })
+ .controller("ThemeController", function ($scope, Theme, $routeParams, $location, $rootScope, $http) {
+ $scope.theme = Theme.get({ themeId: $routeParams.themeId });
+ $scope.fields = ["name", "urlid", "locked", "authors", "headline", "intro", "image", "background_color", "partner_txt", "partner_href", "partner_img"];
+
+ $scope.saveTheme = function () {
+ if (this.theme.id) {
+ this.theme.$update();
+ } else {
+ this.theme.$save(function () {
+ $location.url("/themes/" + $scope.theme.id);
+ });
+ }
+ $rootScope.staticFilesNeedUpdate++;
+ }
+ $scope.deleteTheme = function () {
+ this.theme.$remove(function () {
+ $rootScope.staticFilesNeedUpdate++;
+ $location.url("/themes/");
+ }, function (response) {
+ $scope.addToast('danger', 'An error occurs when trying to delete theme:', response.data.errmsg);
+ });
+ }
+ $scope.checkExoSync = function () {
+ $scope.diff = null;
+ $http({
+ url: "api/themes/" + $scope.theme.id + "/diff-sync",
+ method: "POST"
+ }).then(function (response) {
+ $scope.diff = response.data;
+ if (response.data === null) {
+ $scope.addToast('success', 'Changements par rapport au dépôt', "Tout est pareil !");
+ }
+ }, function (response) {
+ $scope.diff = null;
+ $scope.addToast('danger', 'An error occurs when synchronizing exercice:', response.data.errmsg);
+ });
+ };
+ })
+
+ .controller("TagsListController", function ($scope, $http) {
+ $scope.tags = [];
+ $http({
+ url: "api/tags",
+ method: "GET"
+ }).then(function (response) {
+ $scope.tags = response.data
+ });
+ })
+
+ .controller("AllExercicesListController", function ($scope, Exercice, Theme, $routeParams, $location, $rootScope, $http, $filter) {
+ $http({
+ url: "api/themes.json",
+ method: "GET"
+ }).then(function (response) {
+ $scope.themes = response.data
+ });
+
+ $scope.exercices = Exercice.query();
+ $scope.exercice = {}; // Array used to save fields to updates in selected exercices
+ $scope.fields = ["title", "headline"];
+
+ $scope.validateSearch = function (keyEvent) {
+ if (keyEvent.which === 13) {
+ var myExercice = null;
+ $scope.exercices.forEach(function (exercice) {
+ if (String(exercice.title.toLowerCase()).indexOf($scope.query.toLowerCase()) >= 0) {
+ if (myExercice === null)
+ myExercice = exercice;
+ else
+ myExercice = false;
+ }
+ });
+ if (myExercice)
+ $location.url("exercices/" + myExercice.id);
+ }
+ };
+
+ $scope.toggleSelectAll = function () {
+ angular.forEach($filter('filter')($scope.exercices, $scope.query), function (ex) {
+ ex.selected = !$scope.selectall
+ })
+ }
+
+ $scope.updateExercices = function () {
+ angular.forEach($scope.exercices, function (ex) {
+ if (ex.selected) {
+ Exercice.patch({ exerciceId: ex.id }, $scope.exercice);
+ }
+ })
+ $scope.exercice = {};
+ $rootScope.staticFilesNeedUpdate++;
+ $scope.addToast('success', 'Édition de masse terminée avec succès');
+ }
+
+ $scope.show = function (id) {
+ $location.url("/exercices/" + id);
+ };
+ $scope.inSync = false;
+ $scope.syncFull = function () {
+ $scope.inSync = true;
+ $scope.done = -1;
+ $scope.total = 0;
+ var work = [];
+ var go = function () {
+ if (!work.length) {
+ $scope.addToast('info', "Synchronisation des exercices terminée.");
+ $scope.inSync = false;
+ return;
+ }
+ var u = work.pop();
+
+ $http({
+ url: u,
+ method: "GET"
+ }).then(function (response) {
+ $rootScope.staticFilesNeedUpdate++;
+ $scope.done += 1;
+ go();
+ }, function (response) {
+ $scope.done += 1;
+ go();
+ });
+ };
+
+ angular.forEach($scope.exercices, function (ex) {
+ if ($scope.syncFiles)
+ work.push("api/sync/exercices/" + ex.id + "/files");
+ if ($scope.syncHints)
+ work.push("api/sync/exercices/" + ex.id + "/hints");
+ if ($scope.syncFlags)
+ work.push("api/sync/exercices/" + ex.id + "/flags");
+ });
+ $scope.total = work.length;
+ go();
+
+ };
+ $scope.syncFiles = true;
+ $scope.syncHints = true;
+ $scope.syncFlags = true;
+ })
+ .controller("ExercicesListController", function ($scope, ThemedExercice, $location, $rootScope, $http) {
+ $scope.exercices = ThemedExercice.query({ themeId: $scope.theme.id });
+ $scope.fields = ["title", "headline", "issue"];
+
+ $scope.show = function (id) {
+ $location.url("/themes/" + $scope.theme.id + "/exercices/" + id);
+ };
+
+ $scope.inSync = false;
+ $scope.syncExo = function () {
+ $scope.inSync = true;
+ $http({
+ url: "api/sync/themes/" + $scope.theme.id + "/exercices",
+ method: "POST"
+ }).then(function (response) {
+ $scope.inSync = false;
+ $scope.exercices = ThemedExercice.query({ themeId: $scope.theme.id });
+ $rootScope.staticFilesNeedUpdate++;
+ if (response.data)
+ $scope.addToast('warning', 'An error occurs when synchrinizing exercices:', response.data);
+ else
+ $scope.addToast('success', 'Synchronisation de la liste des exercices terminée avec succès.');
+ }, function (response) {
+ $scope.inSync = false;
+ $scope.addToast('danger', 'An error occurs when synchrinizing exercices:', response.data.errmsg);
+ });
+ };
+ })
+ .controller("ExerciceController", function ($scope, $rootScope, Exercice, ThemedExercice, $routeParams, $location, $http) {
+ if ($routeParams.themeId && $routeParams.exerciceId == "new") {
+ $scope.exercice = new ThemedExercice();
+ } else {
+ $scope.exercice = Exercice.get({ exerciceId: $routeParams.exerciceId });
+ }
+
+ $scope.my_ex_num = {};
+
+ $http({
+ url: "api/themes.json",
+ method: "GET"
+ }).then(function (response) {
+ $scope.themes = response.data;
+ if ($scope.exercice.id_theme) {
+ for (var k in $scope.themes[$scope.exercice.id_theme].exercices) {
+ var exercice = $scope.themes[$scope.exercice.id_theme].exercices[k];
+ $scope.my_ex_num[exercice.id] = k;
+ }
+ } else {
+ for (var k in $scope.themes["0"].exercices) {
+ var exercice = $scope.themes["0"].exercices[k];
+ $scope.my_ex_num[exercice.id] = k;
+ }
+ }
+ });
+ $scope.exercices = Exercice.query();
+ $scope.fields = ["title", "urlid", "authors", "disabled", "statement", "headline", "overview", "finished", "depend", "gain", "coefficient", "videoURI", "image", "background_color", "resolution", "issue", "issuekind", "wip"];
+
+ $scope.inSync = false;
+ $scope.syncExo = function () {
+ $scope.inSync = true;
+ $http({
+ url: $scope.exercice.id_theme ? ("api/sync/themes/" + $scope.exercice.id_theme + "/exercices/" + $routeParams.exerciceId) : ("api/sync/exercices/" + $routeParams.exerciceId),
+ method: "POST"
+ }).then(function (response) {
+ $scope.inSync = false;
+ $scope.exercice = Exercice.get({ exerciceId: $routeParams.exerciceId });
+ $rootScope.staticFilesNeedUpdate++;
+ if (response.data)
+ $scope.addToast('danger', 'An error occurs when synchronizing exercice:', response.data);
+ else
+ $scope.addToast('success', "Synchronisation de l'exercice terminée avec succès.");
+ }, function (response) {
+ $scope.inSync = false;
+ $scope.addToast('danger', 'An error occurs when synchronizing exercice:', response.data.errmsg);
+ });
+ };
+ $scope.checkExoSync = function () {
+ $scope.diff = null;
+ $http({
+ url: ($scope.exercice.id_theme ? ("api/themes/" + $scope.exercice.id_theme + "/exercices/" + $routeParams.exerciceId) : ("api/exercices/" + $routeParams.exerciceId)) + "/diff-sync",
+ method: "POST"
+ }).then(function (response) {
+ $scope.diff = response.data;
+ if (response.data === null) {
+ $scope.addToast('success', 'Changements par rapport au dépôt', "Tout est pareil !");
+ }
+ }, function (response) {
+ $scope.diff = null;
+ $scope.addToast('danger', 'An error occurs when synchronizing exercice:', response.data.errmsg);
+ });
+ };
+
+ $scope.deleteExercice = function () {
+ var tid = $scope.exercice.id_theme;
+ this.exercice.$remove(function () {
+ $rootScope.staticFilesNeedUpdate++;
+ $location.url("/themes/" + tid);
+ }, function (response) {
+ $scope.addToast('danger', 'An error occurs when trying to delete exercice:', response.data.errmsg);
+ });
+ }
+ $scope.saveExercice = function () {
+ if (this.exercice.id) {
+ this.exercice.$update();
+ $rootScope.staticFilesNeedUpdate++;
+ } else if ($routeParams.themeId) {
+ this.exercice.$save({ themeId: $routeParams.themeId }, function () {
+ $rootScope.staticFilesNeedUpdate++;
+ $location.url("/themes/" + $scope.exercice.idTheme + "/exercices/" + $scope.exercice.id);
+ }, function (response) {
+ $scope.addToast('danger', 'An error occurs when trying to save exercice:', response.data.errmsg);
+ });
+ }
+ }
+ $scope.selectedTeam = "";
+ $scope.validateForTeam = function () {
+ var flagid = $("#validationModal").data("flagid");
+ if (!flagid) return;
+ var target = {
+ team_id: parseInt($("#tteam").val().replace(/number:/, '')),
+ kind: $("#validationModal").data("kind"),
+ secondary: flagid,
+ };
+ $http({
+ url: "api/exercices/" + $scope.exercice.id + "/history.json",
+ method: "PUT",
+ data: target
+ }).then(function (response) {
+ $rootScope.staticFilesNeedUpdate++;
+ $("#validationModal").modal('hide');
+ $scope.addToast('success', 'Flag validé avec succès');
+ }, function (response) {
+ $scope.addToast('danger', 'An error occurs when trying to validate flag for team:', response.data.errmsg);
+ });
+ }
+ $scope.historyAppend = function () {
+ var secondary = null;
+ if ($("#historyEvent").val() == "hint")
+ secondary = parseInt($("#historySecondaryHint").val().replace(/number:/, ''));
+ else if ($("#historyEvent").val() == "wchoices" || $("#historyEvent").val() == "flag_found")
+ secondary = parseInt($("#historySecondaryFlag").val().replace(/number:/, ''));
+ else if ($("#historyEvent").val() == "mcq_found")
+ secondary = parseInt($("#historySecondaryQuiz").val().replace(/number:/, ''));
+
+ var target = {
+ team_id: parseInt($("#tteam").val().replace(/number:/, '')),
+ kind: $("#historyEvent").val(),
+ secondary: secondary,
+ };
+ $http({
+ url: "api/exercices/" + $scope.exercice.id + "/history.json",
+ method: "PUT",
+ data: target
+ }).then(function (response) {
+ $rootScope.staticFilesNeedUpdate++;
+ $("#appendHistoryModal").modal('hide');
+ $scope.addToast('success', 'Événement ajouté avec succès');
+ $scope.refreshHistory();
+ }, function (response) {
+ $scope.addToast('danger', 'An error occurs when trying to add event in history:', response.data.errmsg);
+ });
+ }
+ })
+
+ .controller("SearchTryController", function ($scope, ExerciceTries) {
+ $scope.tr = ExerciceTries.get({ exerciceId: $scope.exercice.id, tryId: $scope.row.secondary });
+ })
+
+ .controller("SubmissionsStatsController", function ($scope, $http, $interval) {
+ var refresh = function () {
+ $http({
+ url: "api/submissions-stats.json",
+ }).then(function (response) {
+ $scope.submissionsstats = response.data;
+ });
+ }
+ var myinterval = $interval(refresh, 15000);
+ refresh();
+ $scope.$on('$destroy', function () { $interval.cancel(myinterval); });
+ })
+
+ .controller("ValidationsStatsController", function ($scope, $http, $interval) {
+ var refresh = function () {
+ $http({
+ url: "api/validations-stats.json",
+ }).then(function (response) {
+ $scope.validationsstats = response.data;
+ });
+ }
+ var myinterval = $interval(refresh, 15000);
+ refresh();
+ $scope.$on('$destroy', function () { $interval.cancel(myinterval); });
+ })
+
+ .controller("ExercicesStatsController", function ($scope, Themes, ExercicesStats) {
+ $scope.themes = Themes.get();
+ $scope.exercices = {};
+ ExercicesStats.query().$promise.then(function (exs) {
+ exs.forEach(function (ex) {
+ $scope.exercices[ex.id_exercice] = ex;
+ })
+ });
+ $scope.lenExoArray = [];
+ $scope.themes.$promise.then(function (themes) {
+ if (themes['0'] && themes['0'].exercices.length) {
+ var j = 0;
+ for (var i = themes['0'].exercices.length / 10; i >= 0; i--) {
+ $scope.lenExoArray.push(j++);
+ }
+ }
+ });
+ })
+
+ .controller("ExerciceStatsController", function ($scope, ExerciceStats, $routeParams) {
+ $scope.stats = ExerciceStats.get({ exerciceId: $routeParams.exerciceId });
+ })
+
+ .controller("ExerciceClaimsController", function ($scope, ExerciceClaims, Team, ClaimAssignee, $routeParams, $location) {
+ $scope.claims = ExerciceClaims.query({ exerciceId: $routeParams.exerciceId });
+ $scope.assignees = ClaimAssignee.query();
+
+ $scope.claims.$promise.then(function (claims) {
+ claims.forEach(function (claim, cid) {
+ $scope.claims[cid].team = Team.get({ teamId: claim.id_team });
+ })
+ });
+
+ $scope.showClosed = false;
+ $scope.show = function (id) {
+ $location.url("/claims/" + id);
+ };
+ })
+
+ .controller("ForgeLinksController", function ($scope, $http) {
+ $http({
+ url: "api/exercices_forge_bindings.json",
+ }).then(function (response) {
+ $scope.forge_links = response.data;
+ }, function (response) {
+ $scope.addToast('danger', 'An error occurs when generating exercice forge links: ', response.data.errmsg);
+ });
+ })
+
+ .controller("ExerciceTagsController", function ($scope, ExerciceTags, $routeParams, $rootScope) {
+ $scope.tags = ExerciceTags.query({ exerciceId: $routeParams.exerciceId });
+
+ $scope.addTag = function () {
+ $scope.tags.push("");
+ }
+ $scope.deleteTag = function () {
+ $scope.tags.splice($scope.tags.indexOf(this.tag), 1);
+ return $scope.saveTags();
+ }
+ $scope.saveTags = function () {
+ ExerciceTags.update({ exerciceId: $routeParams.exerciceId }, this.tags);
+ $rootScope.staticFilesNeedUpdate++;
+ }
+ })
+
+ .controller("ExerciceHistoryController", function ($scope, ExerciceHistory, $routeParams, $http, $rootScope) {
+ $scope.history = [];
+ $scope.refreshHistory = function () {
+ $scope.history = ExerciceHistory.query({ exerciceId: $routeParams.exerciceId });
+ }
+ $scope.$parent.refreshHistory = $scope.refreshHistory;
+ $scope.refreshHistory();
+ $scope.updHistory = function () {
+ var target = {
+ team_id: $("#updHistory").data("idteam"),
+ kind: $("#updHistory").data("kind"),
+ time: $("#updHistory").data("time"),
+ secondary: $("#updHistory").data("secondary") != "" ? $("#updHistory").data("secondary") : null,
+ coeff: parseFloat($('#historycoeff').val()),
+ };
+ if (target) {
+ $http({
+ url: "api/exercices/" + $routeParams.exerciceId + "/history.json",
+ method: "PATCH",
+ data: target
+ }).then(function (response) {
+ $rootScope.staticFilesNeedUpdate++;
+ $scope.history = ExerciceHistory.query({ exerciceId: $routeParams.exerciceId });
+ $('#updHistory').modal('hide');
+ }, function (response) {
+ $scope.addToast('danger', 'An error occurs when updating history item: ', response.data.errmsg);
+ });
+ }
+ }
+ $scope.delHistory = function (row) {
+ $http({
+ url: "api/exercices/" + $routeParams.exerciceId + "/history.json",
+ method: "DELETE",
+ data: row
+ }).then(function (response) {
+ $rootScope.staticFilesNeedUpdate++;
+ $scope.history = ExerciceHistory.query({ exerciceId: $routeParams.exerciceId });
+ }, function (response) {
+ $scope.addToast('danger', 'An error occurs when removing history item: ', response.data.errmsg);
+ });
+ }
+ })
+
+ .controller("ExerciceFilesController", function ($scope, ExerciceFile, $routeParams, $rootScope, $http) {
+ $scope.files = ExerciceFile.query({ exerciceId: $routeParams.exerciceId });
+
+ $scope.deleteFile = function () {
+ this.file.$delete(function () {
+ $scope.files = ExerciceFile.query({ exerciceId: $routeParams.exerciceId });
+ });
+ $rootScope.staticFilesNeedUpdate++;
+ return false;
+ }
+ $scope.deleteFileDep = function () {
+ $http({
+ url: "api//files/" + this.file.id + "/dependancies/" + this.dep.id,
+ method: "DELETE"
+ }).then(function (response) {
+ $scope.files = ExerciceFile.query({ exerciceId: $routeParams.exerciceId });
+ }, function (response) {
+ $scope.addToast('danger', 'An error occurs when removing file dep:', response.data.errmsg);
+ });
+ $rootScope.staticFilesNeedUpdate++;
+ return false;
+ }
+ $scope.saveFile = function () {
+ this.file.$update();
+ $rootScope.staticFilesNeedUpdate++;
+ }
+ $scope.inSync = false;
+ $scope.syncFiles = function () {
+ $scope.inSync = true;
+ $http({
+ url: "api/sync/exercices/" + $routeParams.exerciceId + "/files",
+ method: "POST"
+ }).then(function (response) {
+ $scope.inSync = false;
+ $rootScope.staticFilesNeedUpdate++;
+ $scope.files = ExerciceFile.query({ exerciceId: $routeParams.exerciceId });
+ if (response.data)
+ $scope.addToast('danger', 'An error occurs when synchronizing flags list:', response.data);
+ else
+ $scope.addToast('success', "Synchronisation de la liste de fichiers terminée avec succès.");
+ }, function (response) {
+ $scope.inSync = false;
+ $scope.addToast('danger', 'An error occurs when synchronizing flags list:', response.data.errmsg);
+ });
+ };
+ })
+
+ .controller("ExerciceHintsController", function ($scope, ExerciceHint, $routeParams, $rootScope, $http) {
+ $scope.hints = ExerciceHint.query({ exerciceId: $routeParams.exerciceId });
+
+ $scope.addHint = function () {
+ $scope.hints.push(new ExerciceHint());
+ }
+ $scope.deleteHint = function () {
+ this.hint.$delete(function () {
+ $scope.hints = ExerciceHint.query({ exerciceId: $routeParams.exerciceId });
+ $rootScope.staticFilesNeedUpdate++;
+ }, function (response) {
+ $scope.addToast('danger', 'An error occurs when trying to delete hint:', response.data.errmsg);
+ });
+ }
+ $scope.saveHint = function () {
+ if (this.hint.id) {
+ this.hint.$update();
+ } else {
+ this.hint.$save({ exerciceId: $routeParams.exerciceId });
+ }
+ $rootScope.staticFilesNeedUpdate++;
+ }
+ $scope.inSync = false;
+ $scope.syncHints = function () {
+ $scope.inSync = true;
+ $http({
+ url: "api/sync/exercices/" + $routeParams.exerciceId + "/hints",
+ method: "POST"
+ }).then(function (response) {
+ $scope.inSync = false;
+ $scope.hints = ExerciceHint.query({ exerciceId: $routeParams.exerciceId });
+ $rootScope.staticFilesNeedUpdate++;
+ if (response.data)
+ $scope.addToast('danger', 'An error occurs when synchronizing hints list:', response.data);
+ else
+ $scope.addToast('success', "Synchronisation de la liste d'indices terminée avec succès.");
+ }, function (response) {
+ $scope.inSync = false;
+ $scope.addToast('danger', 'An error occurs when synchronizing hints list:', response.data.errmsg);
+ });
+ };
+ })
+
+ .controller("ExerciceHintDepsController", function ($scope, $routeParams, ExerciceHintDeps) {
+ $scope.init = function (hint) {
+ $scope.deps = ExerciceHintDeps.query({ exerciceId: $routeParams.exerciceId, hintId: hint.id });
+ }
+ })
+
+ .controller("ExerciceFlagsController", function ($scope, ExerciceFlag, $routeParams, $rootScope, $http) {
+ $scope.flags = ExerciceFlag.query({ exerciceId: $routeParams.exerciceId });
+
+ $scope.flags.$promise.then(function (flags) {
+ flags.forEach(function (flag, fid) {
+ flags[fid].values = [''];
+ });
+ });
+
+ $scope.changeValue = function (flag) {
+ flag.value = undefined;
+ flag.show_raw = true;
+ }
+ $scope.addFlag = function () {
+ $scope.flags.push(new ExerciceFlag());
+ }
+ $scope.deleteFlag = function () {
+ this.flag.$delete(function () {
+ $scope.flags = ExerciceFlag.query({ exerciceId: $routeParams.exerciceId });
+ $rootScope.staticFilesNeedUpdate++;
+ }, function (response) {
+ $scope.addToast('danger', 'An error occurs when trying to delete flag:', response.data.errmsg);
+ });
+ }
+ $scope.saveFlag = function () {
+ if (this.flag.id) {
+ this.flag.$update().then(function() {}, function(error) { $scope.addToast('danger', 'Impossible de mettre à jour le flag :', error.data.errmsg); });
+ } else {
+ this.flag.$save({ exerciceId: $routeParams.exerciceId }).then(function() {}, function(error) { $scope.addToast('danger', 'Impossible de créer le flag :', error.data.errmsg); });
+ }
+ $rootScope.staticFilesNeedUpdate++;
+ }
+ $scope.testFlag = function (flag) {
+ if (flag.values) {
+ var val = flag.value;
+ treatFlagKey(flag);
+ flag.test_str = flag.value;
+ flag.value = val;
+ }
+
+ if (flag.test_str) {
+ $http({
+ url: "api/exercices/" + $routeParams.exerciceId + "/flags/" + flag.id + "/try",
+ data: { "flag": flag.test_str },
+ method: "POST"
+ }).then(function (response) {
+ flag.test_str = "";
+ $scope.addToast('success', "Flag Ok !");
+ }, function (response) {
+ flag.test_str = "";
+ $scope.addToast('danger', 'An error occurs: ', response.data.errmsg);
+ });
+ }
+ }
+ $scope.inSync = false;
+ $scope.syncFlags = function () {
+ $scope.inSync = true;
+ $http({
+ url: "api/sync/exercices/" + $routeParams.exerciceId + "/flags",
+ method: "POST"
+ }).then(function (response) {
+ $scope.inSync = false;
+ $scope.flags = ExerciceFlag.query({ exerciceId: $routeParams.exerciceId });
+ $rootScope.staticFilesNeedUpdate++;
+ if (response.data)
+ $scope.addToast('danger', 'An error occurs when synchronizing flags list:', response.data);
+ else
+ $scope.addToast('success', "Synchronisation de la liste de drapeaux terminée avec succès.");
+ }, function (response) {
+ $scope.inSync = false;
+ $scope.addToast('danger', 'An error occurs when synchronizing flags list:', response.data.errmsg);
+ });
+ };
+ })
+
+ .controller("ExerciceFlagChoicesController", function ($scope, ExerciceFlagChoices, $routeParams) {
+ $scope.choices = ExerciceFlagChoices.query({ exerciceId: $routeParams.exerciceId, flagId: $scope.flag.id })
+
+ $scope.flag.wantchoices = function () {
+ $scope.flag.choices = {};
+ $scope.choices.forEach(function (choice) {
+ $scope.flag.choices[choice.value] = choice.label
+ })
+ }
+ $scope.choices.$promise.then(function (choices) {
+ if ($scope.flag.choices_cost == 0 && choices.length > 0) {
+ $scope.flag.wantchoices()
+ }
+ })
+
+
+ $scope.addChoice = function () {
+ $scope.choices.push(new ExerciceFlagChoices())
+ }
+
+ $scope.saveChoice = function () {
+ if (this.choice.id)
+ this.choice.$update({ exerciceId: $routeParams.exerciceId, flagId: this.flag.id })
+ else
+ this.choice.$save({ exerciceId: $routeParams.exerciceId, flagId: this.flag.id })
+ }
+
+ $scope.deleteChoice = function () {
+ if (this.choice.id)
+ this.choice.$delete({ exerciceId: $routeParams.exerciceId, flagId: this.flag.id }, function () {
+ $scope.choices = ExerciceFlagChoices.query({ exerciceId: $routeParams.exerciceId, flagId: $scope.flag.id })
+ })
+ }
+ })
+
+ .controller("ExerciceFlagDepsController", function ($scope, $routeParams, ExerciceFlagDeps) {
+ $scope.init = function (flag) {
+ $scope.deps = ExerciceFlagDeps.query({ exerciceId: $routeParams.exerciceId, flagId: flag.id });
+ }
+ })
+
+ .controller("ExerciceFlagStatsController", function ($scope, $routeParams, ExerciceFlagStats, $http) {
+ $scope.init = function (flag) {
+ $scope.flag_id = flag.id;
+ $scope.stats = ExerciceFlagStats.get({ exerciceId: $routeParams.exerciceId, flagId: flag.id });
+ }
+ $scope.deleteTries = function () {
+ $http.delete(`/api/exercices/${$routeParams.exerciceId}/flags/${$scope.flag_id}/tries`).then(function () {
+ $scope.stats = ExerciceFlagStats.get({ exerciceId: $routeParams.exerciceId, flagId: $scope.flag_id });
+ });
+ }
+ })
+
+ .controller("ExerciceMCQFlagsController", function ($scope, ExerciceMCQFlag, $routeParams, $rootScope) {
+ $scope.quiz = ExerciceMCQFlag.query({ exerciceId: $routeParams.exerciceId });
+
+ $scope.addQuiz = function () {
+ $scope.quiz.push(new ExerciceMCQFlag());
+ }
+ $scope.deleteQuiz = function () {
+ this.q.$delete(function () {
+ $scope.quiz = ExerciceMCQFlag.query({ exerciceId: $routeParams.exerciceId });
+ $rootScope.staticFilesNeedUpdate++;
+ }, function (response) {
+ $scope.addToast('danger', 'An error occurs when trying to delete flag:', response.data.errmsg);
+ });
+ }
+ $scope.saveQuiz = function () {
+ if (this.q.id) {
+ this.q.$update();
+ } else {
+ this.q.$save({ exerciceId: $routeParams.exerciceId });
+ }
+ $rootScope.staticFilesNeedUpdate++;
+ }
+
+ $scope.addChoice = function () {
+ this.quiz[this.qk].entries.push({ label: "", response: false })
+ }
+ $scope.deleteChoice = function () {
+ this.quiz[this.qk].entries.splice(this.quiz[this.qk].entries.indexOf(this.choice), 1);
+ }
+ })
+
+ .controller("ExerciceMCQDepsController", function ($scope, $routeParams, ExerciceMCQDeps) {
+ $scope.init = function (flag) {
+ $scope.deps = ExerciceMCQDeps.query({ exerciceId: $routeParams.exerciceId, mcqId: flag.id });
+ }
+ })
+
+ .controller("ExerciceMCQStatsController", function ($scope, $routeParams, ExerciceMCQStats, $http) {
+ $scope.init = function (mcq) {
+ $scope.mcq_id = mcq.id;
+ $scope.stats = ExerciceMCQStats.get({ exerciceId: $routeParams.exerciceId, mcqId: mcq.id });
+ }
+ $scope.deleteTries = function () {
+ $http.delete(`/api/exercices/${$routeParams.exerciceId}/quiz/${$scope.mcq_id}/tries`).then(function () {
+ $scope.stats = ExerciceMCQStats.get({ exerciceId: $routeParams.exerciceId, mcqId: $scope.mcq_id });
+ });
+ }
+ })
+
+ .controller("TeamsListController", function ($scope, $rootScope, Team, $location, $http) {
+ $scope.teams = Team.query();
+ $scope.fields = ["id", "name"];
+
+ $scope.order = [];
+ $scope.teams.$promise.then(function (teams) {
+ teams.forEach(function (team) {
+ $scope.order.push(team.id);
+ });
+ });
+ $scope.validateSearch = function (keyEvent) {
+ if (keyEvent.which === 13) {
+ var myTeam = null;
+ $scope.teams.forEach(function (team) {
+ if (String(team.name.toLowerCase()).indexOf($scope.query.toLowerCase()) >= 0) {
+ if (myTeam === null)
+ myTeam = team;
+ else
+ myTeam = false;
+ }
+ });
+ if (myTeam)
+ $location.url("teams/" + myTeam.id);
+ }
+ };
+
+ $scope.desactiveTeams = function () {
+ $http.post("api/disableinactiveteams").then(function () {
+ $scope.teams = Team.query();
+ }, function (response) {
+ $scope.addToast('danger', 'An error occurs when disabling inactive teams:', response.data.errmsg);
+ });
+ }
+ $scope.refineTeamsColors = function () {
+ $http.post("api/refine_colors").then(function () {
+ $scope.teams = Team.query();
+ }, function (response) {
+ $scope.addToast('danger', 'An error occurs when updating teams:', response.data.errmsg);
+ });
+ }
+ $scope.show = function (id) {
+ $location.url("/teams/" + id);
+ };
+ $scope.triggerTeamsImport = function() {
+ document.getElementById('crTeamsInput').click();
+ };
+ $scope.uploadFile = function() {
+ var formData = new FormData();
+ formData.append('file', $scope.selectedFile);
+
+ $http.post('api/cyberrange-teams.json', formData, {
+ transformRequest: angular.identity,
+ headers: {'Content-Type': undefined},
+ }).then(function(response) {
+ $scope.teams = response.data;
+ $scope.addToast('success', 'Import des équipes', "L'import a été réalisé avec succès !");
+ }, function(error) {
+ console.log(error);
+ $scope.addToast('danger', 'Import des équipes', error.data.errmsg);
+ });
+ };
+ })
+ .controller("TeamMembersController", function ($scope, TeamMember) {
+ $scope.fields = ["firstname", "lastname", "nickname", "company"];
+ if ($scope.team != null) {
+ $scope.members = TeamMember.query({ teamId: $scope.team.id });
+
+ $scope.newMember = function () {
+ $scope.members.push(new TeamMember());
+ }
+ $scope.saveTeamMembers = function () {
+ if (this.team.id) {
+ TeamMember.save({ teamId: this.team.id }, $scope.members);
+ }
+ }
+ $scope.removeMember = function (member) {
+ angular.forEach($scope.members, function (m, k) {
+ if (member == m)
+ $scope.members.splice(k, 1);
+ });
+ }
+ }
+ })
+ .controller("TeamController", function ($scope, $rootScope, $location, Team, TeamMember, $routeParams, $http) {
+ if ($scope.team && $scope.team.id)
+ $routeParams.teamId = $scope.team.id;
+ $scope.team = Team.get({ teamId: $routeParams.teamId });
+ $scope.fields = ["name", "color", "external_id"];
+
+ $scope.saveTeam = function () {
+ if (this.team.id) {
+ this.team.$update();
+ $rootScope.staticFilesNeedUpdate++;
+ } else {
+ this.team.$save(function () {
+ $rootScope.staticFilesNeedUpdate++;
+ $location.url("/teams/" + $scope.team.id);
+ });
+ }
+ }
+ $scope.resetPasswd = function (team) {
+ $http({
+ url: "api/password",
+ method: "POST"
+ }).then(function (response) {
+ team.password = response.data.password;
+ });
+ }
+ $scope.deleteTeam = function () {
+ backName = this.team.name;
+ this.team.$remove(function () { $scope.addToast('success', 'Team ' + backName + ' successfully removed.'); $location.url("/teams/"); $rootScope.staticFilesNeedUpdate++; },
+ function (response) { $scope.addToast('danger', 'An error occurs during suppression of the team:', response.data.errmsg); });
+ }
+ $scope.showStats = function () {
+ $location.url("/teams/" + $scope.team.id + "/stats");
+ }
+ $scope.showScore = function () {
+ $location.url("/teams/" + $scope.team.id + "/score");
+ }
+ })
+ .controller("TeamCertificatesController", function ($scope, $rootScope, TeamCertificate, $routeParams, $http) {
+ $scope.certificates = TeamCertificate.query({ teamId: $routeParams.teamId });
+ $scope.certificates.$promise.then(function (certificates) {
+ certificates.forEach(function (certificate, cid) {
+ certificate.serial = parseInt(certificate.id).toString(16);
+ });
+ });
+
+ $scope.dissociateCertificate = function (certificate) {
+ $http({
+ url: "api/certs/" + certificate.id,
+ method: "PUT",
+ data: {
+ id_team: null
+ }
+ }).then(function (response) {
+ $scope.certificates = TeamCertificate.query({ teamId: $routeParams.teamId }).$promise.then(function (certificates) {
+ certificates.forEach(function (certificate, cid) {
+ certificate.serial = parseInt(certificate.id).toString(16);
+ });
+ });
+ $scope.addToast('success', 'Certificate successfully dissociated!');
+ }, function (response) {
+ $scope.addToast('danger', 'An error occurs when dissociating certiticate:', response.data.errmsg);
+ });
+ }
+ })
+ .controller("TeamAssociationsController", function ($scope, $rootScope, TeamAssociation, $routeParams, $http) {
+ $scope.associations = TeamAssociation.query({ teamId: $routeParams.teamId });
+ $scope.form = { "newassoc": "" };
+
+ $scope.addAssociation = function () {
+ if ($scope.form.newassoc) {
+ TeamAssociation.save({ teamId: $scope.team.id, assoc: $scope.form.newassoc }).$promise.then(
+ function () {
+ $scope.form.newassoc = "";
+ $scope.associations = TeamAssociation.query({ teamId: $routeParams.teamId });
+ }, function (response) {
+ if (response.data)
+ $scope.addToast('danger', 'An error occurs when creating user association: ', response.data.errmsg);
+ });
+ }
+ }
+
+ $scope.dropAssociation = function (assoc) {
+ TeamAssociation.delete({ teamId: $routeParams.teamId, assoc: assoc }).$promise.then(
+ function () {
+ $scope.associations = TeamAssociation.query({ teamId: $routeParams.teamId });
+ }, function (response) {
+ $scope.addToast('danger', 'An error occurs when removing user association: ', response.data.errmsg);
+ });
+ }
+ })
+ .controller("TeamHistoryController", function ($scope, TeamHistory, $routeParams, $http, $rootScope) {
+ $scope.history = TeamHistory.query({ teamId: $routeParams.teamId });
+ $scope.updHistory = function () {
+ var target = {
+ team_id: parseInt($routeParams.teamId),
+ kind: $("#updHistory").data("kind"),
+ time: $("#updHistory").data("time"),
+ secondary: $("#updHistory").data("secondary") != "" ? $("#updHistory").data("secondary") : null,
+ coeff: parseFloat($('#historycoeff').val()),
+ };
+ if (target) {
+ $http({
+ url: "api/exercices/" + $("#updHistory").data("primary") + "/history.json",
+ method: "PATCH",
+ data: target
+ }).then(function (response) {
+ $rootScope.staticFilesNeedUpdate++;
+ $scope.history = TeamHistory.query({ teamId: $routeParams.teamId });
+ $('#updHistory').modal('hide');
+ }, function (response) {
+ $scope.addToast('danger', 'An error occurs when updating history item: ', response.data.errmsg);
+ });
+ }
+ }
+ $scope.delHistory = function (row) {
+ $http({
+ url: "api/teams/" + $routeParams.teamId + "/history.json",
+ method: "DELETE",
+ data: row
+ }).then(function (response) {
+ $rootScope.staticFilesNeedUpdate++;
+ $scope.history = TeamHistory.query({ teamId: $routeParams.teamId });
+ }, function (response) {
+ $scope.addToast('danger', 'An error occurs when removing history item: ', response.data.errmsg);
+ });
+ }
+ })
+ .controller("TeamScoreController", function ($scope, TeamScore, TeamMy, Exercice, $routeParams) {
+ $scope.scores = TeamScore.query({ teamId: $routeParams.teamId });
+ $scope.my = TeamMy.get({ teamId: $routeParams.teamId });
+ $scope.exercices = Exercice.query();
+ })
+ .controller("TeamStatsController", function ($scope, TeamStats, $routeParams) {
+ $scope.teamstats = TeamStats.get({ teamId: $routeParams.teamId });
+ $scope.teamstats.$promise.then(function (res) {
+ solvedByLevelPie("#pieLevels", res.levels);
+ var themes = [];
+ angular.forEach(res.themes, function (theme, tid) {
+ themes.push(theme);
+ })
+ solvedByThemesPie("#pieThemes", themes);
+ });
+ })
+ .controller("TeamsJSONController", function ($scope, Teams) {
+ $scope.teams = Teams.get();
+ $scope.teams.$promise.then(function (teams) {
+ $scope.rank = [];
+ Object.keys(teams).forEach(function (team) {
+ $scope.teams[team].id = team;
+ $scope.rank.push($scope.teams[team]);
+ })
+ });
+ })
+ .controller("TeamExercicesController", function ($scope, Teams, Themes, TeamMy, Exercice, $routeParams) {
+ $scope.teams = Teams.get();
+ $scope.themes = Themes.get();
+ $scope.exercices = Exercice.query();
+ $scope.my = TeamMy.get({ teamId: $routeParams.teamId });
+
+ $scope.teams.$promise.then(function (res) {
+ $scope.nb_teams = 0;
+ $scope.nb_reg_teams = Object.keys(res).length;
+ angular.forEach(res, function (team, tid) {
+ if (team.rank)
+ $scope.nb_teams += 1;
+ }, 0);
+ });
+
+ $scope.my.$promise.then(function (res) {
+ $scope.solved_exercices = 0;
+ angular.forEach(res.exercices, function (exercice, eid) {
+ if (exercice.solved_rank) {
+ $scope.solved_exercices += 1;
+ }
+ }, 0);
+ });
})
- })
- .controller("PresenceController", function($scope, TeamPresence, $routeParams) {
- $scope.presence = TeamPresence.query({ teamId: $routeParams.teamId });
- $scope.presence.$promise.then(function(res) {
- presenceCal("#presenceCal", res);
+ .controller("PresenceController", function ($scope, TeamPresence, $routeParams) {
+ $scope.presence = TeamPresence.query({ teamId: $routeParams.teamId });
+ $scope.presence.$promise.then(function (res) {
+ presenceCal($scope, "#presenceCal", res);
+ });
});
- })
- .controller("CountdownController", function($scope, $http, $timeout) {
- $scope.time = {};
- function updTime() {
- $timeout.cancel($scope.cbm);
- $scope.cbm = $timeout(updTime, 1000);
- if (sessionStorage.userService) {
- var time = angular.fromJson(sessionStorage.userService);
- var srv_cur = (Date.now() + (time.cu * 1000 - time.he)) / 1000;
- var remain = time.du;
- if (time.st == Math.floor(srv_cur)) {
- $scope.refresh(true);
- }
- if (time.st > 0 && time.st <= srv_cur) {
- remain = time.st + time.du - srv_cur;
- }
- if (remain < 0) {
- remain = 0;
- $scope.time.end = true;
- $scope.time.expired = true;
- } else if (remain < 60) {
- $scope.time.end = false;
- $scope.time.expired = true;
- } else {
- $scope.time.end = false;
- $scope.time.expired = false;
- }
- $scope.time.start = time.st * 1000;
- $scope.time.duration = time.du;
- $scope.time.remaining = remain;
- $scope.time.hours = Math.floor(remain / 3600);
- $scope.time.minutes = Math.floor((remain % 3600) / 60);
- $scope.time.seconds = Math.floor(remain % 60);
- }
- }
-
- $http.get("/time.json").success(function(time) {
- time.he = (new Date()).getTime();
- sessionStorage.userService = angular.toJson(time);
- updTime();
- });
- });
function solvedByLevelPie(location, data) {
- var width = d3.select(location).node().getBoundingClientRect().width - 10,
- height = d3.select(location).node().getBoundingClientRect().width - 10,
- radius = Math.min(width, height) / 2,
- innerRadius = 0.1 * radius;
+ var width = d3.select(location).node().getBoundingClientRect().width - parseInt(d3.select(location).style("padding-right")) - parseInt(d3.select(location).style("padding-left")),
+ height = d3.select(location).node().getBoundingClientRect().width - 10,
+ radius = Math.min(width, height) / 2,
+ innerRadius = 0.1 * radius;
- var color = d3.scale.ordinal()
- .range(["#9E0041", "#C32F4B", "#E1514B", "#F47245", "#FB9F59", "#FEC574", "#FAE38C", "#EAD195", "#C7E89E", "#9CD6A4", "#6CC4A4", "#4D9DB4", "#4776B4", "#5E4EA1"]);
+ var color = d3.scale.ordinal()
+ .range(["#9E0041", "#C32F4B", "#E1514B", "#F47245", "#FB9F59", "#FEC574", "#FAE38C", "#EAD195", "#C7E89E", "#9CD6A4", "#6CC4A4", "#4D9DB4", "#4776B4", "#5E4EA1"]);
- var pie = d3.layout.pie()
- .sort(null)
- .value(function(d) { return d.width; });
+ var pie = d3.layout.pie()
+ .sort(null)
+ .value(function (d) { return d.width; });
- var arc = d3.svg.arc()
- .innerRadius(innerRadius)
- .outerRadius(function (d) {
- return (radius - innerRadius) * (d.data.score / 100.0) + innerRadius;
+ var arc = d3.svg.arc()
+ .innerRadius(innerRadius)
+ .outerRadius(function (d) {
+ return (radius - innerRadius) * (d.data.score / 100.0) + innerRadius;
+ });
+
+ var outlineArc = d3.svg.arc()
+ .innerRadius(innerRadius)
+ .outerRadius(radius);
+
+ var svg = d3.select(location).append("svg")
+ .attr("width", width)
+ .attr("height", height)
+ .append("g")
+ .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
+
+ data.forEach(function (d) {
+ d.score = d.solved * 100 / d.total;
+ d.width = d.tries + 1;
});
- var outlineArc = d3.svg.arc()
- .innerRadius(innerRadius)
- .outerRadius(radius);
+ var path = svg.selectAll(".solidArc")
+ .data(pie(data))
+ .enter().append("path")
+ .attr("fill", function (d) { return color(d.data.tip); })
+ .attr("class", "solidArc")
+ .attr("stroke", "gray")
+ .attr("d", arc);
- var svg = d3.select(location).append("svg")
- .attr("width", width)
- .attr("height", height)
- .append("g")
- .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
+ var outerPath = svg.selectAll(".outlineArc")
+ .data(pie(data))
+ .enter().append("path")
+ .attr("fill", "none")
+ .attr("stroke", "gray")
+ .attr("class", "outlineArc")
+ .attr("d", outlineArc);
- data.forEach(function(d) {
- d.score = d.solved * 100 / d.total;
- d.width = d.tries + 1;
- });
+ var labelArc = d3.svg.arc()
+ .outerRadius(0.8 * radius)
+ .innerRadius(0.8 * radius);
- var path = svg.selectAll(".solidArc")
- .data(pie(data))
- .enter().append("path")
- .attr("fill", function(d) { return color(d.data.tip); })
- .attr("class", "solidArc")
- .attr("stroke", "gray")
- .attr("d", arc);
+ svg.selectAll(".labelArc")
+ .data(pie(data))
+ .enter().append("text")
+ .attr("transform", function (d) { return "translate(" + labelArc.centroid(d) + ")"; })
+ .attr("dy", ".35em")
+ .attr("text-anchor", "middle")
+ .text(function (d) { return d.data.tip + ": " + d.data.solved + "/" + d.data.total; });
- var outerPath = svg.selectAll(".outlineArc")
- .data(pie(data))
- .enter().append("path")
- .attr("fill", "none")
- .attr("stroke", "gray")
- .attr("class", "outlineArc")
- .attr("d", outlineArc);
-
- var labelArc = d3.svg.arc()
- .outerRadius(0.8 * radius)
- .innerRadius(0.8 * radius);
-
- svg.selectAll(".labelArc")
- .data(pie(data))
- .enter().append("text")
- .attr("transform", function(d) { return "translate(" + labelArc.centroid(d) + ")"; })
- .attr("dy", ".35em")
- .attr("text-anchor", "middle")
- .text(function(d) { return d.data.tip + ": " + d.data.solved + "/" + d.data.total; });
-
- svg.selectAll(".label2Arc")
- .data(pie(data))
- .enter().append("text")
- .attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; })
- .attr("dy", ".35em")
- .attr("text-anchor", "middle")
- .text(function(d) { return d.data.tries; });
+ svg.selectAll(".label2Arc")
+ .data(pie(data))
+ .enter().append("text")
+ .attr("transform", function (d) { return "translate(" + arc.centroid(d) + ")"; })
+ .attr("dy", ".35em")
+ .attr("text-anchor", "middle")
+ .text(function (d) { return d.data.tries; });
}
function solvedByThemesPie(location, data) {
- var width = d3.select(location).node().getBoundingClientRect().width,
- height = d3.select(location).node().getBoundingClientRect().width,
- radius = Math.min(width, height) / 2,
- innerRadius = 0.1 * radius;
+ var width = d3.select(location).node().getBoundingClientRect().width - parseInt(d3.select(location).style("padding-right")) - parseInt(d3.select(location).style("padding-left")),
+ height = d3.select(location).node().getBoundingClientRect().width,
+ radius = Math.min(width, height) / 2,
+ innerRadius = 0.1 * radius;
- var color = d3.scale.ordinal()
- .range(["#9E0041", "#C32F4B", "#E1514B", "#F47245", "#FB9F59", "#FEC574", "#FAE38C", "#EAD195", "#C7E89E", "#9CD6A4", "#6CC4A4", "#4D9DB4", "#4776B4", "#5E4EA1"]);
+ var color = d3.scale.ordinal()
+ .range(["#9E0041", "#C32F4B", "#E1514B", "#F47245", "#FB9F59", "#FEC574", "#FAE38C", "#EAD195", "#C7E89E", "#9CD6A4", "#6CC4A4", "#4D9DB4", "#4776B4", "#5E4EA1"]);
- var pie = d3.layout.pie()
- .sort(null)
- .value(function(d) { return d.width; });
+ var pie = d3.layout.pie()
+ .sort(null)
+ .value(function (d) { return d.width; });
- var arc = d3.svg.arc()
- .innerRadius(innerRadius)
- .outerRadius(function (d) {
- return (radius - innerRadius) * (d.data.score / 100.0) + innerRadius;
+ var arc = d3.svg.arc()
+ .innerRadius(innerRadius)
+ .outerRadius(function (d) {
+ return (radius - innerRadius) * (d.data.score / 100.0) + innerRadius;
+ });
+
+ var outlineArc = d3.svg.arc()
+ .innerRadius(innerRadius)
+ .outerRadius(radius);
+
+ var svg = d3.select(location).append("svg")
+ .attr("width", width)
+ .attr("height", height)
+ .append("g")
+ .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
+
+ data.forEach(function (d) {
+ d.score = d.solved * 100 / d.total;
+ d.width = d.tries + 0.5;
});
- var outlineArc = d3.svg.arc()
- .innerRadius(innerRadius)
- .outerRadius(radius);
+ var path = svg.selectAll(".solidArc")
+ .data(pie(data))
+ .enter().append("path")
+ .attr("fill", function (d) { return color(d.data.tip); })
+ .attr("class", "solidArc")
+ .attr("stroke", "gray")
+ .attr("d", arc);
- var svg = d3.select(location).append("svg")
- .attr("width", width)
- .attr("height", height)
- .append("g")
- .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
+ var outerPath = svg.selectAll(".outlineArc")
+ .data(pie(data))
+ .enter().append("path")
+ .attr("fill", "none")
+ .attr("stroke", "gray")
+ .attr("class", "outlineArc")
+ .attr("d", outlineArc);
- data.forEach(function(d) {
- d.score = d.solved * 100 / d.total;
- d.width = d.tries + 0.5;
- });
+ svg.selectAll(".label2Arc")
+ .data(pie(data))
+ .enter().append("text")
+ .attr("transform", function (d) { return "translate(" + arc.centroid(d) + ")"; })
+ .attr("dy", ".35em")
+ .attr("text-anchor", "middle")
+ .text(function (d) { return d.data.solved; });
- var path = svg.selectAll(".solidArc")
- .data(pie(data))
- .enter().append("path")
- .attr("fill", function(d) { return color(d.data.tip); })
- .attr("class", "solidArc")
- .attr("stroke", "gray")
- .attr("d", arc);
+ var labelArc = d3.svg.arc()
+ .outerRadius(0.8 * radius)
+ .innerRadius(0.8 * radius);
- var outerPath = svg.selectAll(".outlineArc")
- .data(pie(data))
- .enter().append("path")
- .attr("fill", "none")
- .attr("stroke", "gray")
- .attr("class", "outlineArc")
- .attr("d", outlineArc);
-
- svg.selectAll(".label2Arc")
- .data(pie(data))
- .enter().append("text")
- .attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; })
- .attr("dy", ".35em")
- .attr("text-anchor", "middle")
- .text(function(d) { return d.data.solved; });
-
- var labelArc = d3.svg.arc()
- .outerRadius(0.8 * radius)
- .innerRadius(0.8 * radius);
-
- svg.selectAll(".labelArc")
- .data(pie(data))
- .enter().append("text")
- .attr("transform", function(d) { return "translate(" + labelArc.centroid(d) + ")"; })
- .attr("dy", ".35em")
- .attr("text-anchor", "middle")
- .text(function(d) { return d.data.tip + ": " + d.data.tries; });
+ svg.selectAll(".labelArc")
+ .data(pie(data))
+ .enter().append("text")
+ .attr("transform", function (d) { return "translate(" + labelArc.centroid(d) + ")"; })
+ .attr("dy", ".35em")
+ .attr("text-anchor", "middle")
+ .text(function (d) { return d.data.tip + ": " + d.data.tries; });
}
-function presenceCal(location, data) {
- var width = d3.select(location).node().getBoundingClientRect().width,
- height = 80,
- cellSize = 17; // cell size
+function presenceCal(scope, location, data) {
+ var width = d3.select(location).node().getBoundingClientRect().width,
+ height = 80,
+ cellSize = 17; // cell size
- var percent = d3.format(".1%"),
- format = d3.time.format("%H:%M");
+ var percent = d3.format(".1%"),
+ format = d3.time.format("%H:%M");
- var color = d3.scale.quantize()
- .domain([0, 16])
- .range(d3.range(8).map(function(d) { return "q" + d + "-8"; }));
+ var color = d3.scale.quantize()
+ .domain([0, 16])
+ .range(d3.range(8).map(function (d) { return "q" + d + "-8"; }));
- var svg = d3.select(location).selectAll("svg")
- .data(d3.range(26, 29))
- .enter().append("svg")
- .attr("width", width)
- .attr("height", height)
- .attr("class", "RdYlGn")
- .append("g")
- .attr("transform", "translate(" + ((width - cellSize * 24) / 2) + "," + (height - cellSize * 4 - 1) + ")");
+ var svg = d3.select(location).selectAll("svg")
+ .data(d3.range(scope.settings.start, scope.time.start + (scope.settings.start % 86400000 + scope.settings.end - scope.settings.start), 86400000).map(function (t) { return new Date(t); }))
+ .enter().append("svg")
+ .attr("width", width)
+ .attr("height", height)
+ .attr("class", "RdYlGn")
+ .append("g")
+ .attr("transform", "translate(" + ((width - cellSize * 24) / 2) + "," + (height - cellSize * 4 - 1) + ")");
- svg.append("text")
- .attr("transform", "translate(-6," + cellSize * 2.6 + ")rotate(-90)")
- .style("text-anchor", "middle")
- .text(function(d) { return d + "-02"; });
+ svg.append("text")
+ .attr("transform", "translate(-6," + cellSize * 2.6 + ")rotate(-90)")
+ .style("text-anchor", "middle")
+ .text(function (d) { return d.getDate() + "-" + (d.getMonth() + 1); });
- var rect = svg.selectAll(".quarter")
- .data(function(d) { return d3.time.minutes(new Date(2016, 1, d, 0), new Date(2016, 1, d, 24), 15); })
- .enter().append("rect")
- .attr("width", cellSize)
- .attr("height", cellSize)
- .attr("class", function(d) { return color(data.reduce(function(prev, cur){
- cur = new Date(cur).getTime();
- dv = d.getTime();
- return prev + ((dv <= cur && cur < dv+15*60000)?1:0);
- }, 0)); });
+ var rect = svg.selectAll(".quarter")
+ .data(function (d) { return d3.time.minutes(new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0), new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23), 15); })
+ .enter().append("rect")
+ .attr("width", cellSize)
+ .attr("height", cellSize)
+ .attr("transform", function (d) { return "translate(" + (d.getHours() * cellSize) + "," + (d.getMinutes() / 15 * cellSize) + ")"; })
+ .attr("class", function (d) {
+ if (d >= scope.settings.start && d < scope.settings.start + scope.settings.end - scope.settings.start) return color(data.reduce(function (prev, cur) {
+ cur = new Date(cur).getTime();
+ dv = d.getTime();
+ return prev + ((dv <= cur && cur < dv + 15 * 60000) ? 1 : 0);
+ }, 0));
+ });
}
diff --git a/admin/static/js/bootstrap.min.js b/admin/static/js/bootstrap.min.js
deleted file mode 120000
index d074c9db..00000000
--- a/admin/static/js/bootstrap.min.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../frontend/static/js/bootstrap.min.js
\ No newline at end of file
diff --git a/admin/static/js/bootstrap.min.js b/admin/static/js/bootstrap.min.js
new file mode 100644
index 00000000..97206dcd
--- /dev/null
+++ b/admin/static/js/bootstrap.min.js
@@ -0,0 +1,7 @@
+/*!
+ * Bootstrap v4.6.2 (https://getbootstrap.com/)
+ * Copyright 2011-2022 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
+ */
+!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jquery"),require("popper.js")):"function"==typeof define&&define.amd?define(["exports","jquery","popper.js"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap={},t.jQuery,t.Popper)}(this,(function(t,e,n){"use strict";function i(t){return t&&"object"==typeof t&&"default"in t?t:{default:t}}var o=i(e),a=i(n);function s(t,e){for(var n=0;n=4)throw new Error("Bootstrap's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0")}};d.jQueryDetection(),o.default.fn.emulateTransitionEnd=function(t){var e=this,n=!1;return o.default(this).one(d.TRANSITION_END,(function(){n=!0})),setTimeout((function(){n||d.triggerTransitionEnd(e)}),t),this},o.default.event.special[d.TRANSITION_END]={bindType:f,delegateType:f,handle:function(t){if(o.default(t.target).is(this))return t.handleObj.handler.apply(this,arguments)}};var c="bs.alert",h=o.default.fn.alert,g=function(){function t(t){this._element=t}var e=t.prototype;return e.close=function(t){var e=this._element;t&&(e=this._getRootElement(t)),this._triggerCloseEvent(e).isDefaultPrevented()||this._removeElement(e)},e.dispose=function(){o.default.removeData(this._element,c),this._element=null},e._getRootElement=function(t){var e=d.getSelectorFromElement(t),n=!1;return e&&(n=document.querySelector(e)),n||(n=o.default(t).closest(".alert")[0]),n},e._triggerCloseEvent=function(t){var e=o.default.Event("close.bs.alert");return o.default(t).trigger(e),e},e._removeElement=function(t){var e=this;if(o.default(t).removeClass("show"),o.default(t).hasClass("fade")){var n=d.getTransitionDurationFromElement(t);o.default(t).one(d.TRANSITION_END,(function(n){return e._destroyElement(t,n)})).emulateTransitionEnd(n)}else this._destroyElement(t)},e._destroyElement=function(t){o.default(t).detach().trigger("closed.bs.alert").remove()},t._jQueryInterface=function(e){return this.each((function(){var n=o.default(this),i=n.data(c);i||(i=new t(this),n.data(c,i)),"close"===e&&i[e](this)}))},t._handleDismiss=function(t){return function(e){e&&e.preventDefault(),t.close(this)}},l(t,null,[{key:"VERSION",get:function(){return"4.6.2"}}]),t}();o.default(document).on("click.bs.alert.data-api",'[data-dismiss="alert"]',g._handleDismiss(new g)),o.default.fn.alert=g._jQueryInterface,o.default.fn.alert.Constructor=g,o.default.fn.alert.noConflict=function(){return o.default.fn.alert=h,g._jQueryInterface};var m="bs.button",p=o.default.fn.button,_="active",v='[data-toggle^="button"]',y='input:not([type="hidden"])',b=".btn",E=function(){function t(t){this._element=t,this.shouldAvoidTriggerChange=!1}var e=t.prototype;return e.toggle=function(){var t=!0,e=!0,n=o.default(this._element).closest('[data-toggle="buttons"]')[0];if(n){var i=this._element.querySelector(y);if(i){if("radio"===i.type)if(i.checked&&this._element.classList.contains(_))t=!1;else{var a=n.querySelector(".active");a&&o.default(a).removeClass(_)}t&&("checkbox"!==i.type&&"radio"!==i.type||(i.checked=!this._element.classList.contains(_)),this.shouldAvoidTriggerChange||o.default(i).trigger("change")),i.focus(),e=!1}}this._element.hasAttribute("disabled")||this._element.classList.contains("disabled")||(e&&this._element.setAttribute("aria-pressed",!this._element.classList.contains(_)),t&&o.default(this._element).toggleClass(_))},e.dispose=function(){o.default.removeData(this._element,m),this._element=null},t._jQueryInterface=function(e,n){return this.each((function(){var i=o.default(this),a=i.data(m);a||(a=new t(this),i.data(m,a)),a.shouldAvoidTriggerChange=n,"toggle"===e&&a[e]()}))},l(t,null,[{key:"VERSION",get:function(){return"4.6.2"}}]),t}();o.default(document).on("click.bs.button.data-api",v,(function(t){var e=t.target,n=e;if(o.default(e).hasClass("btn")||(e=o.default(e).closest(b)[0]),!e||e.hasAttribute("disabled")||e.classList.contains("disabled"))t.preventDefault();else{var i=e.querySelector(y);if(i&&(i.hasAttribute("disabled")||i.classList.contains("disabled")))return void t.preventDefault();"INPUT"!==n.tagName&&"LABEL"===e.tagName||E._jQueryInterface.call(o.default(e),"toggle","INPUT"===n.tagName)}})).on("focus.bs.button.data-api blur.bs.button.data-api",v,(function(t){var e=o.default(t.target).closest(b)[0];o.default(e).toggleClass("focus",/^focus(in)?$/.test(t.type))})),o.default(window).on("load.bs.button.data-api",(function(){for(var t=[].slice.call(document.querySelectorAll('[data-toggle="buttons"] .btn')),e=0,n=t.length;e0,this._pointerEvent=Boolean(window.PointerEvent||window.MSPointerEvent),this._addEventListeners()}var e=t.prototype;return e.next=function(){this._isSliding||this._slide(N)},e.nextWhenVisible=function(){var t=o.default(this._element);!document.hidden&&t.is(":visible")&&"hidden"!==t.css("visibility")&&this.next()},e.prev=function(){this._isSliding||this._slide(D)},e.pause=function(t){t||(this._isPaused=!0),this._element.querySelector(".carousel-item-next, .carousel-item-prev")&&(d.triggerTransitionEnd(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null},e.cycle=function(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config.interval&&!this._isPaused&&(this._updateInterval(),this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))},e.to=function(t){var e=this;this._activeElement=this._element.querySelector(I);var n=this._getItemIndex(this._activeElement);if(!(t>this._items.length-1||t<0))if(this._isSliding)o.default(this._element).one(A,(function(){return e.to(t)}));else{if(n===t)return this.pause(),void this.cycle();var i=t>n?N:D;this._slide(i,this._items[t])}},e.dispose=function(){o.default(this._element).off(".bs.carousel"),o.default.removeData(this._element,w),this._items=null,this._config=null,this._element=null,this._interval=null,this._isPaused=null,this._isSliding=null,this._activeElement=null,this._indicatorsElement=null},e._getConfig=function(t){return t=r({},k,t),d.typeCheckConfig(T,t,O),t},e._handleSwipe=function(){var t=Math.abs(this.touchDeltaX);if(!(t<=40)){var e=t/this.touchDeltaX;this.touchDeltaX=0,e>0&&this.prev(),e<0&&this.next()}},e._addEventListeners=function(){var t=this;this._config.keyboard&&o.default(this._element).on("keydown.bs.carousel",(function(e){return t._keydown(e)})),"hover"===this._config.pause&&o.default(this._element).on("mouseenter.bs.carousel",(function(e){return t.pause(e)})).on("mouseleave.bs.carousel",(function(e){return t.cycle(e)})),this._config.touch&&this._addTouchEventListeners()},e._addTouchEventListeners=function(){var t=this;if(this._touchSupported){var e=function(e){t._pointerEvent&&j[e.originalEvent.pointerType.toUpperCase()]?t.touchStartX=e.originalEvent.clientX:t._pointerEvent||(t.touchStartX=e.originalEvent.touches[0].clientX)},n=function(e){t._pointerEvent&&j[e.originalEvent.pointerType.toUpperCase()]&&(t.touchDeltaX=e.originalEvent.clientX-t.touchStartX),t._handleSwipe(),"hover"===t._config.pause&&(t.pause(),t.touchTimeout&&clearTimeout(t.touchTimeout),t.touchTimeout=setTimeout((function(e){return t.cycle(e)}),500+t._config.interval))};o.default(this._element.querySelectorAll(".carousel-item img")).on("dragstart.bs.carousel",(function(t){return t.preventDefault()})),this._pointerEvent?(o.default(this._element).on("pointerdown.bs.carousel",(function(t){return e(t)})),o.default(this._element).on("pointerup.bs.carousel",(function(t){return n(t)})),this._element.classList.add("pointer-event")):(o.default(this._element).on("touchstart.bs.carousel",(function(t){return e(t)})),o.default(this._element).on("touchmove.bs.carousel",(function(e){return function(e){t.touchDeltaX=e.originalEvent.touches&&e.originalEvent.touches.length>1?0:e.originalEvent.touches[0].clientX-t.touchStartX}(e)})),o.default(this._element).on("touchend.bs.carousel",(function(t){return n(t)})))}},e._keydown=function(t){if(!/input|textarea/i.test(t.target.tagName))switch(t.which){case 37:t.preventDefault(),this.prev();break;case 39:t.preventDefault(),this.next()}},e._getItemIndex=function(t){return this._items=t&&t.parentNode?[].slice.call(t.parentNode.querySelectorAll(".carousel-item")):[],this._items.indexOf(t)},e._getItemByDirection=function(t,e){var n=t===N,i=t===D,o=this._getItemIndex(e),a=this._items.length-1;if((i&&0===o||n&&o===a)&&!this._config.wrap)return e;var s=(o+(t===D?-1:1))%this._items.length;return-1===s?this._items[this._items.length-1]:this._items[s]},e._triggerSlideEvent=function(t,e){var n=this._getItemIndex(t),i=this._getItemIndex(this._element.querySelector(I)),a=o.default.Event("slide.bs.carousel",{relatedTarget:t,direction:e,from:i,to:n});return o.default(this._element).trigger(a),a},e._setActiveIndicatorElement=function(t){if(this._indicatorsElement){var e=[].slice.call(this._indicatorsElement.querySelectorAll(".active"));o.default(e).removeClass(S);var n=this._indicatorsElement.children[this._getItemIndex(t)];n&&o.default(n).addClass(S)}},e._updateInterval=function(){var t=this._activeElement||this._element.querySelector(I);if(t){var e=parseInt(t.getAttribute("data-interval"),10);e?(this._config.defaultInterval=this._config.defaultInterval||this._config.interval,this._config.interval=e):this._config.interval=this._config.defaultInterval||this._config.interval}},e._slide=function(t,e){var n,i,a,s=this,l=this._element.querySelector(I),r=this._getItemIndex(l),u=e||l&&this._getItemByDirection(t,l),f=this._getItemIndex(u),c=Boolean(this._interval);if(t===N?(n="carousel-item-left",i="carousel-item-next",a="left"):(n="carousel-item-right",i="carousel-item-prev",a="right"),u&&o.default(u).hasClass(S))this._isSliding=!1;else if(!this._triggerSlideEvent(u,a).isDefaultPrevented()&&l&&u){this._isSliding=!0,c&&this.pause(),this._setActiveIndicatorElement(u),this._activeElement=u;var h=o.default.Event(A,{relatedTarget:u,direction:a,from:r,to:f});if(o.default(this._element).hasClass("slide")){o.default(u).addClass(i),d.reflow(u),o.default(l).addClass(n),o.default(u).addClass(n);var g=d.getTransitionDurationFromElement(l);o.default(l).one(d.TRANSITION_END,(function(){o.default(u).removeClass(n+" "+i).addClass(S),o.default(l).removeClass("active "+i+" "+n),s._isSliding=!1,setTimeout((function(){return o.default(s._element).trigger(h)}),0)})).emulateTransitionEnd(g)}else o.default(l).removeClass(S),o.default(u).addClass(S),this._isSliding=!1,o.default(this._element).trigger(h);c&&this.cycle()}},t._jQueryInterface=function(e){return this.each((function(){var n=o.default(this).data(w),i=r({},k,o.default(this).data());"object"==typeof e&&(i=r({},i,e));var a="string"==typeof e?e:i.slide;if(n||(n=new t(this,i),o.default(this).data(w,n)),"number"==typeof e)n.to(e);else if("string"==typeof a){if("undefined"==typeof n[a])throw new TypeError('No method named "'+a+'"');n[a]()}else i.interval&&i.ride&&(n.pause(),n.cycle())}))},t._dataApiClickHandler=function(e){var n=d.getSelectorFromElement(this);if(n){var i=o.default(n)[0];if(i&&o.default(i).hasClass("carousel")){var a=r({},o.default(i).data(),o.default(this).data()),s=this.getAttribute("data-slide-to");s&&(a.interval=!1),t._jQueryInterface.call(o.default(i),a),s&&o.default(i).data(w).to(s),e.preventDefault()}}},l(t,null,[{key:"VERSION",get:function(){return"4.6.2"}},{key:"Default",get:function(){return k}}]),t}();o.default(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",P._dataApiClickHandler),o.default(window).on("load.bs.carousel.data-api",(function(){for(var t=[].slice.call(document.querySelectorAll('[data-ride="carousel"]')),e=0,n=t.length;e0&&(this._selector=s,this._triggerArray.push(a))}this._parent=this._config.parent?this._getParent():null,this._config.parent||this._addAriaAndCollapsedClass(this._element,this._triggerArray),this._config.toggle&&this.toggle()}var e=t.prototype;return e.toggle=function(){o.default(this._element).hasClass(q)?this.hide():this.show()},e.show=function(){var e,n,i=this;if(!(this._isTransitioning||o.default(this._element).hasClass(q)||(this._parent&&0===(e=[].slice.call(this._parent.querySelectorAll(".show, .collapsing")).filter((function(t){return"string"==typeof i._config.parent?t.getAttribute("data-parent")===i._config.parent:t.classList.contains(F)}))).length&&(e=null),e&&(n=o.default(e).not(this._selector).data(R))&&n._isTransitioning))){var a=o.default.Event("show.bs.collapse");if(o.default(this._element).trigger(a),!a.isDefaultPrevented()){e&&(t._jQueryInterface.call(o.default(e).not(this._selector),"hide"),n||o.default(e).data(R,null));var s=this._getDimension();o.default(this._element).removeClass(F).addClass(Q),this._element.style[s]=0,this._triggerArray.length&&o.default(this._triggerArray).removeClass(B).attr("aria-expanded",!0),this.setTransitioning(!0);var l="scroll"+(s[0].toUpperCase()+s.slice(1)),r=d.getTransitionDurationFromElement(this._element);o.default(this._element).one(d.TRANSITION_END,(function(){o.default(i._element).removeClass(Q).addClass("collapse show"),i._element.style[s]="",i.setTransitioning(!1),o.default(i._element).trigger("shown.bs.collapse")})).emulateTransitionEnd(r),this._element.style[s]=this._element[l]+"px"}}},e.hide=function(){var t=this;if(!this._isTransitioning&&o.default(this._element).hasClass(q)){var e=o.default.Event("hide.bs.collapse");if(o.default(this._element).trigger(e),!e.isDefaultPrevented()){var n=this._getDimension();this._element.style[n]=this._element.getBoundingClientRect()[n]+"px",d.reflow(this._element),o.default(this._element).addClass(Q).removeClass("collapse show");var i=this._triggerArray.length;if(i>0)for(var a=0;a0},e._getOffset=function(){var t=this,e={};return"function"==typeof this._config.offset?e.fn=function(e){return e.offsets=r({},e.offsets,t._config.offset(e.offsets,t._element)),e}:e.offset=this._config.offset,e},e._getPopperConfig=function(){var t={placement:this._getPlacement(),modifiers:{offset:this._getOffset(),flip:{enabled:this._config.flip},preventOverflow:{boundariesElement:this._config.boundary}}};return"static"===this._config.display&&(t.modifiers.applyStyle={enabled:!1}),r({},t,this._config.popperConfig)},t._jQueryInterface=function(e){return this.each((function(){var n=o.default(this).data(K);if(n||(n=new t(this,"object"==typeof e?e:null),o.default(this).data(K,n)),"string"==typeof e){if("undefined"==typeof n[e])throw new TypeError('No method named "'+e+'"');n[e]()}}))},t._clearMenus=function(e){if(!e||3!==e.which&&("keyup"!==e.type||9===e.which))for(var n=[].slice.call(document.querySelectorAll(it)),i=0,a=n.length;i0&&s--,40===e.which&&s document.documentElement.clientHeight;n||(this._element.style.overflowY="hidden"),this._element.classList.add(ht);var i=d.getTransitionDurationFromElement(this._dialog);o.default(this._element).off(d.TRANSITION_END),o.default(this._element).one(d.TRANSITION_END,(function(){t._element.classList.remove(ht),n||o.default(t._element).one(d.TRANSITION_END,(function(){t._element.style.overflowY=""})).emulateTransitionEnd(t._element,i)})).emulateTransitionEnd(i),this._element.focus()}},e._showElement=function(t){var e=this,n=o.default(this._element).hasClass(dt),i=this._dialog?this._dialog.querySelector(".modal-body"):null;this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE||document.body.appendChild(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),o.default(this._dialog).hasClass("modal-dialog-scrollable")&&i?i.scrollTop=0:this._element.scrollTop=0,n&&d.reflow(this._element),o.default(this._element).addClass(ct),this._config.focus&&this._enforceFocus();var a=o.default.Event("shown.bs.modal",{relatedTarget:t}),s=function(){e._config.focus&&e._element.focus(),e._isTransitioning=!1,o.default(e._element).trigger(a)};if(n){var l=d.getTransitionDurationFromElement(this._dialog);o.default(this._dialog).one(d.TRANSITION_END,s).emulateTransitionEnd(l)}else s()},e._enforceFocus=function(){var t=this;o.default(document).off(pt).on(pt,(function(e){document!==e.target&&t._element!==e.target&&0===o.default(t._element).has(e.target).length&&t._element.focus()}))},e._setEscapeEvent=function(){var t=this;this._isShown?o.default(this._element).on(yt,(function(e){t._config.keyboard&&27===e.which?(e.preventDefault(),t.hide()):t._config.keyboard||27!==e.which||t._triggerBackdropTransition()})):this._isShown||o.default(this._element).off(yt)},e._setResizeEvent=function(){var t=this;this._isShown?o.default(window).on(_t,(function(e){return t.handleUpdate(e)})):o.default(window).off(_t)},e._hideModal=function(){var t=this;this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._showBackdrop((function(){o.default(document.body).removeClass(ft),t._resetAdjustments(),t._resetScrollbar(),o.default(t._element).trigger(gt)}))},e._removeBackdrop=function(){this._backdrop&&(o.default(this._backdrop).remove(),this._backdrop=null)},e._showBackdrop=function(t){var e=this,n=o.default(this._element).hasClass(dt)?dt:"";if(this._isShown&&this._config.backdrop){if(this._backdrop=document.createElement("div"),this._backdrop.className="modal-backdrop",n&&this._backdrop.classList.add(n),o.default(this._backdrop).appendTo(document.body),o.default(this._element).on(vt,(function(t){e._ignoreBackdropClick?e._ignoreBackdropClick=!1:t.target===t.currentTarget&&("static"===e._config.backdrop?e._triggerBackdropTransition():e.hide())})),n&&d.reflow(this._backdrop),o.default(this._backdrop).addClass(ct),!t)return;if(!n)return void t();var i=d.getTransitionDurationFromElement(this._backdrop);o.default(this._backdrop).one(d.TRANSITION_END,t).emulateTransitionEnd(i)}else if(!this._isShown&&this._backdrop){o.default(this._backdrop).removeClass(ct);var a=function(){e._removeBackdrop(),t&&t()};if(o.default(this._element).hasClass(dt)){var s=d.getTransitionDurationFromElement(this._backdrop);o.default(this._backdrop).one(d.TRANSITION_END,a).emulateTransitionEnd(s)}else a()}else t&&t()},e._adjustDialog=function(){var t=this._element.scrollHeight>document.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},e._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},e._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=Math.round(t.left+t.right)
',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent",customClass:"",sanitize:!0,sanitizeFn:null,whiteList:{"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},popperConfig:null},Ut={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"(number|string|function)",container:"(string|element|boolean)",fallbackPlacement:"(string|array)",boundary:"(string|element)",customClass:"(string|function)",sanitize:"boolean",sanitizeFn:"(null|function)",whiteList:"object",popperConfig:"(null|object)"},Mt={HIDE:"hide.bs.tooltip",HIDDEN:"hidden.bs.tooltip",SHOW:"show.bs.tooltip",SHOWN:"shown.bs.tooltip",INSERTED:"inserted.bs.tooltip",CLICK:"click.bs.tooltip",FOCUSIN:"focusin.bs.tooltip",FOCUSOUT:"focusout.bs.tooltip",MOUSEENTER:"mouseenter.bs.tooltip",MOUSELEAVE:"mouseleave.bs.tooltip"},Wt=function(){function t(t,e){if("undefined"==typeof a.default)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var e=t.prototype;return e.enable=function(){this._isEnabled=!0},e.disable=function(){this._isEnabled=!1},e.toggleEnabled=function(){this._isEnabled=!this._isEnabled},e.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=o.default(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),o.default(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(o.default(this.getTipElement()).hasClass(Rt))return void this._leave(null,this);this._enter(null,this)}},e.dispose=function(){clearTimeout(this._timeout),o.default.removeData(this.element,this.constructor.DATA_KEY),o.default(this.element).off(this.constructor.EVENT_KEY),o.default(this.element).closest(".modal").off("hide.bs.modal",this._hideModalHandler),this.tip&&o.default(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,this._activeTrigger=null,this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},e.show=function(){var t=this;if("none"===o.default(this.element).css("display"))throw new Error("Please use show on visible elements");var e=o.default.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){o.default(this.element).trigger(e);var n=d.findShadowRoot(this.element),i=o.default.contains(null!==n?n:this.element.ownerDocument.documentElement,this.element);if(e.isDefaultPrevented()||!i)return;var s=this.getTipElement(),l=d.getUID(this.constructor.NAME);s.setAttribute("id",l),this.element.setAttribute("aria-describedby",l),this.setContent(),this.config.animation&&o.default(s).addClass(Lt);var r="function"==typeof this.config.placement?this.config.placement.call(this,s,this.element):this.config.placement,u=this._getAttachment(r);this.addAttachmentClass(u);var f=this._getContainer();o.default(s).data(this.constructor.DATA_KEY,this),o.default.contains(this.element.ownerDocument.documentElement,this.tip)||o.default(s).appendTo(f),o.default(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new a.default(this.element,s,this._getPopperConfig(u)),o.default(s).addClass(Rt),o.default(s).addClass(this.config.customClass),"ontouchstart"in document.documentElement&&o.default(document.body).children().on("mouseover",null,o.default.noop);var c=function(){t.config.animation&&t._fixTransition();var e=t._hoverState;t._hoverState=null,o.default(t.element).trigger(t.constructor.Event.SHOWN),e===qt&&t._leave(null,t)};if(o.default(this.tip).hasClass(Lt)){var h=d.getTransitionDurationFromElement(this.tip);o.default(this.tip).one(d.TRANSITION_END,c).emulateTransitionEnd(h)}else c()}},e.hide=function(t){var e=this,n=this.getTipElement(),i=o.default.Event(this.constructor.Event.HIDE),a=function(){e._hoverState!==xt&&n.parentNode&&n.parentNode.removeChild(n),e._cleanTipClass(),e.element.removeAttribute("aria-describedby"),o.default(e.element).trigger(e.constructor.Event.HIDDEN),null!==e._popper&&e._popper.destroy(),t&&t()};if(o.default(this.element).trigger(i),!i.isDefaultPrevented()){if(o.default(n).removeClass(Rt),"ontouchstart"in document.documentElement&&o.default(document.body).children().off("mouseover",null,o.default.noop),this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1,o.default(this.tip).hasClass(Lt)){var s=d.getTransitionDurationFromElement(n);o.default(n).one(d.TRANSITION_END,a).emulateTransitionEnd(s)}else a();this._hoverState=""}},e.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},e.isWithContent=function(){return Boolean(this.getTitle())},e.addAttachmentClass=function(t){o.default(this.getTipElement()).addClass("bs-tooltip-"+t)},e.getTipElement=function(){return this.tip=this.tip||o.default(this.config.template)[0],this.tip},e.setContent=function(){var t=this.getTipElement();this.setElementContent(o.default(t.querySelectorAll(".tooltip-inner")),this.getTitle()),o.default(t).removeClass("fade show")},e.setElementContent=function(t,e){"object"!=typeof e||!e.nodeType&&!e.jquery?this.config.html?(this.config.sanitize&&(e=At(e,this.config.whiteList,this.config.sanitizeFn)),t.html(e)):t.text(e):this.config.html?o.default(e).parent().is(t)||t.empty().append(e):t.text(o.default(e).text())},e.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},e._getPopperConfig=function(t){var e=this;return r({},{placement:t,modifiers:{offset:this._getOffset(),flip:{behavior:this.config.fallbackPlacement},arrow:{element:".arrow"},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){return e._handlePopperPlacementChange(t)}},this.config.popperConfig)},e._getOffset=function(){var t=this,e={};return"function"==typeof this.config.offset?e.fn=function(e){return e.offsets=r({},e.offsets,t.config.offset(e.offsets,t.element)),e}:e.offset=this.config.offset,e},e._getContainer=function(){return!1===this.config.container?document.body:d.isElement(this.config.container)?o.default(this.config.container):o.default(document).find(this.config.container)},e._getAttachment=function(t){return Bt[t.toUpperCase()]},e._setListeners=function(){var t=this;this.config.trigger.split(" ").forEach((function(e){if("click"===e)o.default(t.element).on(t.constructor.Event.CLICK,t.config.selector,(function(e){return t.toggle(e)}));else if("manual"!==e){var n=e===Ft?t.constructor.Event.MOUSEENTER:t.constructor.Event.FOCUSIN,i=e===Ft?t.constructor.Event.MOUSELEAVE:t.constructor.Event.FOCUSOUT;o.default(t.element).on(n,t.config.selector,(function(e){return t._enter(e)})).on(i,t.config.selector,(function(e){return t._leave(e)}))}})),this._hideModalHandler=function(){t.element&&t.hide()},o.default(this.element).closest(".modal").on("hide.bs.modal",this._hideModalHandler),this.config.selector?this.config=r({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},e._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},e._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||o.default(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),o.default(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?Qt:Ft]=!0),o.default(e.getTipElement()).hasClass(Rt)||e._hoverState===xt?e._hoverState=xt:(clearTimeout(e._timeout),e._hoverState=xt,e.config.delay&&e.config.delay.show?e._timeout=setTimeout((function(){e._hoverState===xt&&e.show()}),e.config.delay.show):e.show())},e._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||o.default(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),o.default(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?Qt:Ft]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=qt,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout((function(){e._hoverState===qt&&e.hide()}),e.config.delay.hide):e.hide())},e._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},e._getConfig=function(t){var e=o.default(this.element).data();return Object.keys(e).forEach((function(t){-1!==Pt.indexOf(t)&&delete e[t]})),"number"==typeof(t=r({},this.constructor.Default,e,"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),d.typeCheckConfig(It,t,this.constructor.DefaultType),t.sanitize&&(t.template=At(t.template,t.whiteList,t.sanitizeFn)),t},e._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},e._cleanTipClass=function(){var t=o.default(this.getTipElement()),e=t.attr("class").match(jt);null!==e&&e.length&&t.removeClass(e.join(""))},e._handlePopperPlacementChange=function(t){this.tip=t.instance.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},e._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(o.default(t).removeClass(Lt),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},t._jQueryInterface=function(e){return this.each((function(){var n=o.default(this),i=n.data(kt),a="object"==typeof e&&e;if((i||!/dispose|hide/.test(e))&&(i||(i=new t(this,a),n.data(kt,i)),"string"==typeof e)){if("undefined"==typeof i[e])throw new TypeError('No method named "'+e+'"');i[e]()}}))},l(t,null,[{key:"VERSION",get:function(){return"4.6.2"}},{key:"Default",get:function(){return Ht}},{key:"NAME",get:function(){return It}},{key:"DATA_KEY",get:function(){return kt}},{key:"Event",get:function(){return Mt}},{key:"EVENT_KEY",get:function(){return".bs.tooltip"}},{key:"DefaultType",get:function(){return Ut}}]),t}();o.default.fn.tooltip=Wt._jQueryInterface,o.default.fn.tooltip.Constructor=Wt,o.default.fn.tooltip.noConflict=function(){return o.default.fn.tooltip=Ot,Wt._jQueryInterface};var Vt="bs.popover",zt=o.default.fn.popover,Kt=new RegExp("(^|\\s)bs-popover\\S+","g"),Xt=r({},Wt.Default,{placement:"right",trigger:"click",content:"",template:''}),Yt=r({},Wt.DefaultType,{content:"(string|element|function)"}),$t={HIDE:"hide.bs.popover",HIDDEN:"hidden.bs.popover",SHOW:"show.bs.popover",SHOWN:"shown.bs.popover",INSERTED:"inserted.bs.popover",CLICK:"click.bs.popover",FOCUSIN:"focusin.bs.popover",FOCUSOUT:"focusout.bs.popover",MOUSEENTER:"mouseenter.bs.popover",MOUSELEAVE:"mouseleave.bs.popover"},Jt=function(t){var e,n;function i(){return t.apply(this,arguments)||this}n=t,(e=i).prototype=Object.create(n.prototype),e.prototype.constructor=e,u(e,n);var a=i.prototype;return a.isWithContent=function(){return this.getTitle()||this._getContent()},a.addAttachmentClass=function(t){o.default(this.getTipElement()).addClass("bs-popover-"+t)},a.getTipElement=function(){return this.tip=this.tip||o.default(this.config.template)[0],this.tip},a.setContent=function(){var t=o.default(this.getTipElement());this.setElementContent(t.find(".popover-header"),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(".popover-body"),e),t.removeClass("fade show")},a._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},a._cleanTipClass=function(){var t=o.default(this.getTipElement()),e=t.attr("class").match(Kt);null!==e&&e.length>0&&t.removeClass(e.join(""))},i._jQueryInterface=function(t){return this.each((function(){var e=o.default(this).data(Vt),n="object"==typeof t?t:null;if((e||!/dispose|hide/.test(t))&&(e||(e=new i(this,n),o.default(this).data(Vt,e)),"string"==typeof t)){if("undefined"==typeof e[t])throw new TypeError('No method named "'+t+'"');e[t]()}}))},l(i,null,[{key:"VERSION",get:function(){return"4.6.2"}},{key:"Default",get:function(){return Xt}},{key:"NAME",get:function(){return"popover"}},{key:"DATA_KEY",get:function(){return Vt}},{key:"Event",get:function(){return $t}},{key:"EVENT_KEY",get:function(){return".bs.popover"}},{key:"DefaultType",get:function(){return Yt}}]),i}(Wt);o.default.fn.popover=Jt._jQueryInterface,o.default.fn.popover.Constructor=Jt,o.default.fn.popover.noConflict=function(){return o.default.fn.popover=zt,Jt._jQueryInterface};var Gt="scrollspy",Zt="bs.scrollspy",te=o.default.fn[Gt],ee="active",ne="position",ie=".nav, .list-group",oe={offset:10,method:"auto",target:""},ae={offset:"number",method:"string",target:"(string|element)"},se=function(){function t(t,e){var n=this;this._element=t,this._scrollElement="BODY"===t.tagName?window:t,this._config=this._getConfig(e),this._selector=this._config.target+" .nav-link,"+this._config.target+" .list-group-item,"+this._config.target+" .dropdown-item",this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,o.default(this._scrollElement).on("scroll.bs.scrollspy",(function(t){return n._process(t)})),this.refresh(),this._process()}var e=t.prototype;return e.refresh=function(){var t=this,e=this._scrollElement===this._scrollElement.window?"offset":ne,n="auto"===this._config.method?e:this._config.method,i=n===ne?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),[].slice.call(document.querySelectorAll(this._selector)).map((function(t){var e,a=d.getSelectorFromElement(t);if(a&&(e=document.querySelector(a)),e){var s=e.getBoundingClientRect();if(s.width||s.height)return[o.default(e)[n]().top+i,a]}return null})).filter(Boolean).sort((function(t,e){return t[0]-e[0]})).forEach((function(e){t._offsets.push(e[0]),t._targets.push(e[1])}))},e.dispose=function(){o.default.removeData(this._element,Zt),o.default(this._scrollElement).off(".bs.scrollspy"),this._element=null,this._scrollElement=null,this._config=null,this._selector=null,this._offsets=null,this._targets=null,this._activeTarget=null,this._scrollHeight=null},e._getConfig=function(t){if("string"!=typeof(t=r({},oe,"object"==typeof t&&t?t:{})).target&&d.isElement(t.target)){var e=o.default(t.target).attr("id");e||(e=d.getUID(Gt),o.default(t.target).attr("id",e)),t.target="#"+e}return d.typeCheckConfig(Gt,t,ae),t},e._getScrollTop=function(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop},e._getScrollHeight=function(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)},e._getOffsetHeight=function(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height},e._process=function(){var t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),n=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=n){var i=this._targets[this._targets.length-1];this._activeTarget!==i&&this._activate(i)}else{if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(var o=this._offsets.length;o--;)this._activeTarget!==this._targets[o]&&t>=this._offsets[o]&&("undefined"==typeof this._offsets[o+1]||t li > .active",ge=function(){function t(t){this._element=t}var e=t.prototype;return e.show=function(){var t=this;if(!(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&o.default(this._element).hasClass(ue)||o.default(this._element).hasClass("disabled")||this._element.hasAttribute("disabled"))){var e,n,i=o.default(this._element).closest(".nav, .list-group")[0],a=d.getSelectorFromElement(this._element);if(i){var s="UL"===i.nodeName||"OL"===i.nodeName?he:ce;n=(n=o.default.makeArray(o.default(i).find(s)))[n.length-1]}var l=o.default.Event("hide.bs.tab",{relatedTarget:this._element}),r=o.default.Event("show.bs.tab",{relatedTarget:n});if(n&&o.default(n).trigger(l),o.default(this._element).trigger(r),!r.isDefaultPrevented()&&!l.isDefaultPrevented()){a&&(e=document.querySelector(a)),this._activate(this._element,i);var u=function(){var e=o.default.Event("hidden.bs.tab",{relatedTarget:t._element}),i=o.default.Event("shown.bs.tab",{relatedTarget:n});o.default(n).trigger(e),o.default(t._element).trigger(i)};e?this._activate(e,e.parentNode,u):u()}}},e.dispose=function(){o.default.removeData(this._element,le),this._element=null},e._activate=function(t,e,n){var i=this,a=(!e||"UL"!==e.nodeName&&"OL"!==e.nodeName?o.default(e).children(ce):o.default(e).find(he))[0],s=n&&a&&o.default(a).hasClass(fe),l=function(){return i._transitionComplete(t,a,n)};if(a&&s){var r=d.getTransitionDurationFromElement(a);o.default(a).removeClass(de).one(d.TRANSITION_END,l).emulateTransitionEnd(r)}else l()},e._transitionComplete=function(t,e,n){if(e){o.default(e).removeClass(ue);var i=o.default(e.parentNode).find("> .dropdown-menu .active")[0];i&&o.default(i).removeClass(ue),"tab"===e.getAttribute("role")&&e.setAttribute("aria-selected",!1)}o.default(t).addClass(ue),"tab"===t.getAttribute("role")&&t.setAttribute("aria-selected",!0),d.reflow(t),t.classList.contains(fe)&&t.classList.add(de);var a=t.parentNode;if(a&&"LI"===a.nodeName&&(a=a.parentNode),a&&o.default(a).hasClass("dropdown-menu")){var s=o.default(t).closest(".dropdown")[0];if(s){var l=[].slice.call(s.querySelectorAll(".dropdown-toggle"));o.default(l).addClass(ue)}t.setAttribute("aria-expanded",!0)}n&&n()},t._jQueryInterface=function(e){return this.each((function(){var n=o.default(this),i=n.data(le);if(i||(i=new t(this),n.data(le,i)),"string"==typeof e){if("undefined"==typeof i[e])throw new TypeError('No method named "'+e+'"');i[e]()}}))},l(t,null,[{key:"VERSION",get:function(){return"4.6.2"}}]),t}();o.default(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"], [data-toggle="list"]',(function(t){t.preventDefault(),ge._jQueryInterface.call(o.default(this),"show")})),o.default.fn.tab=ge._jQueryInterface,o.default.fn.tab.Constructor=ge,o.default.fn.tab.noConflict=function(){return o.default.fn.tab=re,ge._jQueryInterface};var me="bs.toast",pe=o.default.fn.toast,_e="hide",ve="show",ye="showing",be="click.dismiss.bs.toast",Ee={animation:!0,autohide:!0,delay:500},Te={animation:"boolean",autohide:"boolean",delay:"number"},we=function(){function t(t,e){this._element=t,this._config=this._getConfig(e),this._timeout=null,this._setListeners()}var e=t.prototype;return e.show=function(){var t=this,e=o.default.Event("show.bs.toast");if(o.default(this._element).trigger(e),!e.isDefaultPrevented()){this._clearTimeout(),this._config.animation&&this._element.classList.add("fade");var n=function(){t._element.classList.remove(ye),t._element.classList.add(ve),o.default(t._element).trigger("shown.bs.toast"),t._config.autohide&&(t._timeout=setTimeout((function(){t.hide()}),t._config.delay))};if(this._element.classList.remove(_e),d.reflow(this._element),this._element.classList.add(ye),this._config.animation){var i=d.getTransitionDurationFromElement(this._element);o.default(this._element).one(d.TRANSITION_END,n).emulateTransitionEnd(i)}else n()}},e.hide=function(){if(this._element.classList.contains(ve)){var t=o.default.Event("hide.bs.toast");o.default(this._element).trigger(t),t.isDefaultPrevented()||this._close()}},e.dispose=function(){this._clearTimeout(),this._element.classList.contains(ve)&&this._element.classList.remove(ve),o.default(this._element).off(be),o.default.removeData(this._element,me),this._element=null,this._config=null},e._getConfig=function(t){return t=r({},Ee,o.default(this._element).data(),"object"==typeof t&&t?t:{}),d.typeCheckConfig("toast",t,this.constructor.DefaultType),t},e._setListeners=function(){var t=this;o.default(this._element).on(be,'[data-dismiss="toast"]',(function(){return t.hide()}))},e._close=function(){var t=this,e=function(){t._element.classList.add(_e),o.default(t._element).trigger("hidden.bs.toast")};if(this._element.classList.remove(ve),this._config.animation){var n=d.getTransitionDurationFromElement(this._element);o.default(this._element).one(d.TRANSITION_END,e).emulateTransitionEnd(n)}else e()},e._clearTimeout=function(){clearTimeout(this._timeout),this._timeout=null},t._jQueryInterface=function(e){return this.each((function(){var n=o.default(this),i=n.data(me);if(i||(i=new t(this,"object"==typeof e&&e),n.data(me,i)),"string"==typeof e){if("undefined"==typeof i[e])throw new TypeError('No method named "'+e+'"');i[e](this)}}))},l(t,null,[{key:"VERSION",get:function(){return"4.6.2"}},{key:"DefaultType",get:function(){return Te}},{key:"Default",get:function(){return Ee}}]),t}();o.default.fn.toast=we._jQueryInterface,o.default.fn.toast.Constructor=we,o.default.fn.toast.noConflict=function(){return o.default.fn.toast=pe,we._jQueryInterface},t.Alert=g,t.Button=E,t.Carousel=P,t.Collapse=V,t.Dropdown=lt,t.Modal=Ct,t.Popover=Jt,t.Scrollspy=se,t.Tab=ge,t.Toast=we,t.Tooltip=Wt,t.Util=d,Object.defineProperty(t,"__esModule",{value:!0})}));
+//# sourceMappingURL=bootstrap.min.js.map
\ No newline at end of file
diff --git a/admin/static/js/common.js b/admin/static/js/common.js
new file mode 100644
index 00000000..08eff2b1
--- /dev/null
+++ b/admin/static/js/common.js
@@ -0,0 +1,355 @@
+var alertNbLines = true;
+
+function treatFlagKey(flag) {
+ if (flag.values !== undefined) {
+ if (flag.separator) {
+ for (var i = flag.values.length - 1; i >= 0; i--) {
+ if (flag.nb_lines && (flag.values[i] == undefined || !flag.values[i].length)) {
+ if (alertNbLines) {
+ alertNbLines = false;
+ if (!confirm("Lorsque plusieurs flags sont attendus pour une même question, ceux-ci ne sont pas validés un par un. Ils ne sont validés qu'une fois tous les champs remplis correctement. (Sauf mention contraire, l'ordre n'importe pas)"))
+ console.log(flag.values[9999].length); // Launch exception here to avoid form validation
+ }
+ }
+ else if (!flag.values[i].length) {
+ flag.values.splice(i, 1);
+ }
+ }
+
+ if (flag.ignore_order)
+ flag.value = flag.values.slice().sort().join(flag.separator) + flag.separator;
+ else
+ flag.value = flag.values.join(flag.separator) + flag.separator;
+
+ if (flag.values.length == 0)
+ flag.values = [""];
+ }
+ else
+ flag.value = flag.values[0];
+ }
+
+ if (flag.found == null && flag.soluce !== undefined) {
+ if (flag.value && flag.soluce) {
+ if (flag.ignore_case)
+ flag.value = flag.value.toLowerCase();
+ if (flag.capture_regexp) {
+ var re = new RegExp(flag.capture_regexp, flag.ignore_case?'ui':'u');
+ var match = re.exec(flag.value);
+ match.shift();
+ flag.value = match.join("+");
+ }
+
+ if (flag.soluce == b2sum(flag.value))
+ flag.found = new Date();
+ }
+ }
+ return flag.found !== undefined && flag.found !== false;
+}
+
+String.prototype.capitalize = function() {
+ return this
+ .toLowerCase()
+ .replace(
+ /(^|\s|-)([a-z])/g,
+ function(m,p1,p2) { return p1+p2.toUpperCase(); }
+ );
+}
+
+Array.prototype.inArray = function(v) {
+ return this.reduce(function(presence, current) {
+ return presence || current == v;
+ }, false);
+}
+
+angular.module("FICApp")
+ .directive('autofocus', ['$timeout', function($timeout) {
+ return {
+ restrict: 'A',
+ link : function($scope, $element) {
+ $timeout(function() {
+ $element[0].focus();
+ });
+ }
+ }
+ }])
+ .directive('autocarousel', ['$timeout', function($timeout) {
+ return {
+ restrict: 'A',
+ link : function($scope, $element) {
+ $timeout(function() {
+ $($element[0]).carousel();
+ });
+ }
+ }
+ }])
+ .directive('fileModel', ['$parse', function ($parse) {
+ return {
+ restrict: 'A',
+ link: function($scope, element, attrs) {
+ var model = $parse(attrs.fileModel);
+ var modelSetter = model.assign;
+
+ element.bind('change', function(){
+ $scope.$apply(function(){
+ modelSetter($scope, element[0].files[0]);
+ $scope.uploadFile();
+ });
+ });
+ }
+ };
+ }]);
+
+angular.module("FICApp")
+ .filter("escapeURL", function() {
+ return function(input) {
+ return encodeURIComponent(input);
+ }
+ })
+ .filter("stripHTML", function() {
+ return function(input) {
+ if (!input)
+ return input;
+ return input.replace(
+ /(<([^>]+)>)/ig,
+ ""
+ );
+ }
+ })
+ .filter("capitalize", function() {
+ return function(input) {
+ return input.capitalize();
+ }
+ })
+ .filter("rankTitle", function() {
+ var itms = {
+ "rank": "Rang",
+ "name": "Équipe",
+ "score": "Score",
+ };
+ return function(input) {
+ if (itms[input] != undefined) {
+ return itms[input];
+ } else {
+ return input;
+ }
+ }
+ })
+ .filter("time", function() {
+ return function(input) {
+ input = Math.floor(input);
+ if (input == undefined) {
+ return "--";
+ } else if (input >= 10) {
+ return input;
+ } else {
+ return "0" + input;
+ }
+ }
+ })
+ .filter("timer", function() {
+ return function(input) {
+ input = Math.floor(input / 1000);
+ var res = ""
+
+ if (input >= 3600) {
+ res += Math.floor(input / 3600) + ":";
+ input = input % 3600;
+ }
+ if (res || input >= 60) {
+ if (res && Math.floor(input / 60) <= 9)
+ res += "0";
+ res += Math.floor(input / 60) + "'";
+ input = input % 60;
+ }
+
+ return res + (input>9?input:"0"+input) + '"';
+ }
+ })
+ .filter("since", function() {
+ return function(passed) {
+ if (passed < 120000) {
+ return "Il y a " + Math.floor(passed/1000) + " secondes";
+ } else {
+ return "Il y a " + Math.floor(passed/60000) + " minutes";
+ }
+ }
+ })
+ .filter("size", function() {
+ var units = [
+ "o",
+ "kio",
+ "Mio",
+ "Gio",
+ "Tio",
+ "Pio",
+ "Eio",
+ "Zio",
+ "Yio",
+ ]
+ return function(input) {
+ var res = input;
+ var unit = 0;
+ while (res > 1024) {
+ unit += 1;
+ res = res / 1024;
+ }
+ return (Math.round(res * 100) / 100) + " " + units[unit];
+ }
+ })
+
+ .filter("coeff", function() {
+ return function(input) {
+ if (input > 1) {
+ return "+" + Math.floor((input - 1) * 100) + " %"
+ } else if (input < 1) {
+ return "-" + Math.floor((1 - input) * 100) + " %"
+ } else {
+ return "";
+ }
+ }
+ })
+
+ .filter("objectLength", function() {
+ return function(input) {
+ if (input !== undefined)
+ return Object.keys(input).length;
+ else
+ return "";
+ }
+ })
+
+ .filter("bto16", function() {
+ return function(input) {
+ const raw = atob(input);
+ let result = '';
+ for (let i = 0; i < raw.length; i++) {
+ const hex = raw.charCodeAt(i).toString(16);
+ result += (hex.length === 2 ? hex : '0' + hex);
+ }
+ return result;
+ }
+ });
+
+angular.module("FICApp")
+ .component('flagKey', {
+ bindings: {
+ kid: '=',
+ key: '=',
+ settings: '=',
+ wantchoices: '=',
+ },
+ controller: function() {
+ this.additem = function(key) {
+ this.key.values.push("");
+ };
+ },
+ template: `
+
+ `
+ });
+
+angular.module("FICApp")
+ .run(function($rootScope) {
+ $rootScope.recvTime = function(response) {
+ time = {
+ "cu": Math.floor(response.headers("x-fic-time") * 1000),
+ "he": (new Date()).getTime(),
+ };
+ sessionStorage.time = angular.toJson(time);
+ return time;
+ }
+ })
+
+ .controller("CountdownController", function($scope, $rootScope, $interval) {
+ var time;
+ if (sessionStorage.time)
+ time = angular.fromJson(sessionStorage.time);
+
+ $scope.time = {};
+
+ $rootScope.getSrvTime = function() {
+ if (time && time.cu && time.he)
+ return new Date(Date.now() + (time.cu - time.he));
+ else
+ return undefined;
+ }
+
+ function updTime() {
+ if (time && $rootScope.settings && $rootScope.settings.end) {
+ var srv_cur = new Date(Date.now() + (time.cu - time.he));
+
+ // Refresh on start/activate time reached
+ if (Math.floor($rootScope.settings.start / 1000) == Math.floor(srv_cur / 1000) ||Math.floor($rootScope.settings.activateTime / 1000) == Math.floor(srv_cur / 1000))
+ $rootScope.refresh(true, true);
+
+ var remain = 0;
+ if ($rootScope.settings.start === undefined || $rootScope.settings.start == 0) {
+ $scope.time = {};
+ return
+ } else if ($rootScope.settings.start > srv_cur) {
+ $scope.startIn = Math.floor(($rootScope.settings.start - srv_cur) / 1000);
+ remain = $rootScope.settings.end - $rootScope.settings.start;
+ } else if ($rootScope.settings.end > srv_cur) {
+ $scope.startIn = 0;
+ remain = $rootScope.settings.end - srv_cur;
+ }
+
+ $rootScope.timeProgression = 1 - remain / ($rootScope.settings.end - $rootScope.settings.start);
+ $rootScope.timeRemaining = remain;
+
+ if ($rootScope.settings.activateTime) {
+ var now = new Date();
+ var actTime = new Date($rootScope.settings.activateTime);
+
+ if (actTime > now)
+ $rootScope.activateTimeCountDown = actTime - now;
+ else
+ $rootScope.activateTimeCountDown = null;
+ } else {
+ $rootScope.activateTimeCountDown = null;
+ }
+
+ remain = remain / 1000;
+
+ if (remain < 0) {
+ remain = 0;
+ $scope.time.end = true;
+ $scope.time.expired = true;
+ } else if (remain < 60) {
+ $scope.time.end = false;
+ $scope.time.expired = true;
+ } else {
+ $scope.time.end = false;
+ $scope.time.expired = false;
+ }
+
+ $scope.time.remaining = remain;
+ $scope.time.hours = Math.floor(remain / 3600);
+ $scope.time.minutes = Math.floor((remain % 3600) / 60);
+ $scope.time.seconds = Math.floor(remain % 60);
+ }
+ }
+ updTime();
+ $interval(updTime, 1000);
+ })
diff --git a/admin/static/js/d3.v3.min.js b/admin/static/js/d3.v3.min.js
deleted file mode 120000
index d2407e47..00000000
--- a/admin/static/js/d3.v3.min.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../frontend/static/js/d3.v3.min.js
\ No newline at end of file
diff --git a/admin/static/js/d3.v3.min.js b/admin/static/js/d3.v3.min.js
new file mode 100644
index 00000000..16648730
--- /dev/null
+++ b/admin/static/js/d3.v3.min.js
@@ -0,0 +1,5 @@
+!function(){function n(n){return n&&(n.ownerDocument||n.document||n).documentElement}function t(n){return n&&(n.ownerDocument&&n.ownerDocument.defaultView||n.document&&n||n.defaultView)}function e(n,t){return t>n?-1:n>t?1:n>=t?0:NaN}function r(n){return null===n?NaN:+n}function i(n){return!isNaN(n)}function u(n){return{left:function(t,e,r,i){for(arguments.length<3&&(r=0),arguments.length<4&&(i=t.length);i>r;){var u=r+i>>>1;n(t[u],e)<0?r=u+1:i=u}return r},right:function(t,e,r,i){for(arguments.length<3&&(r=0),arguments.length<4&&(i=t.length);i>r;){var u=r+i>>>1;n(t[u],e)>0?i=u:r=u+1}return r}}}function o(n){return n.length}function a(n){for(var t=1;n*t%1;)t*=10;return t}function l(n,t){for(var e in t)Object.defineProperty(n.prototype,e,{value:t[e],enumerable:!1})}function c(){this._=Object.create(null)}function f(n){return(n+="")===bo||n[0]===_o?_o+n:n}function s(n){return(n+="")[0]===_o?n.slice(1):n}function h(n){return f(n)in this._}function p(n){return(n=f(n))in this._&&delete this._[n]}function g(){var n=[];for(var t in this._)n.push(s(t));return n}function v(){var n=0;for(var t in this._)++n;return n}function d(){for(var n in this._)return!1;return!0}function y(){this._=Object.create(null)}function m(n){return n}function M(n,t,e){return function(){var r=e.apply(t,arguments);return r===t?n:r}}function x(n,t){if(t in n)return t;t=t.charAt(0).toUpperCase()+t.slice(1);for(var e=0,r=wo.length;r>e;++e){var i=wo[e]+t;if(i in n)return i}}function b(){}function _(){}function w(n){function t(){for(var t,r=e,i=-1,u=r.length;++ie;e++)for(var i,u=n[e],o=0,a=u.length;a>o;o++)(i=u[o])&&t(i,o,e);return n}function Z(n){return ko(n,qo),n}function V(n){var t,e;return function(r,i,u){var o,a=n[u].update,l=a.length;for(u!=e&&(e=u,t=0),i>=t&&(t=i+1);!(o=a[t])&&++t0&&(n=n.slice(0,a));var c=To.get(n);return c&&(n=c,l=B),a?t?i:r:t?b:u}function $(n,t){return function(e){var r=ao.event;ao.event=e,t[0]=this.__data__;try{n.apply(this,t)}finally{ao.event=r}}}function B(n,t){var e=$(n,t);return function(n){var t=this,r=n.relatedTarget;r&&(r===t||8&r.compareDocumentPosition(t))||e.call(t,n)}}function W(e){var r=".dragsuppress-"+ ++Do,i="click"+r,u=ao.select(t(e)).on("touchmove"+r,S).on("dragstart"+r,S).on("selectstart"+r,S);if(null==Ro&&(Ro="onselectstart"in e?!1:x(e.style,"userSelect")),Ro){var o=n(e).style,a=o[Ro];o[Ro]="none"}return function(n){if(u.on(r,null),Ro&&(o[Ro]=a),n){var t=function(){u.on(i,null)};u.on(i,function(){S(),t()},!0),setTimeout(t,0)}}}function J(n,e){e.changedTouches&&(e=e.changedTouches[0]);var r=n.ownerSVGElement||n;if(r.createSVGPoint){var i=r.createSVGPoint();if(0>Po){var u=t(n);if(u.scrollX||u.scrollY){r=ao.select("body").append("svg").style({position:"absolute",top:0,left:0,margin:0,padding:0,border:"none"},"important");var o=r[0][0].getScreenCTM();Po=!(o.f||o.e),r.remove()}}return Po?(i.x=e.pageX,i.y=e.pageY):(i.x=e.clientX,i.y=e.clientY),i=i.matrixTransform(n.getScreenCTM().inverse()),[i.x,i.y]}var a=n.getBoundingClientRect();return[e.clientX-a.left-n.clientLeft,e.clientY-a.top-n.clientTop]}function G(){return ao.event.changedTouches[0].identifier}function K(n){return n>0?1:0>n?-1:0}function Q(n,t,e){return(t[0]-n[0])*(e[1]-n[1])-(t[1]-n[1])*(e[0]-n[0])}function nn(n){return n>1?0:-1>n?Fo:Math.acos(n)}function tn(n){return n>1?Io:-1>n?-Io:Math.asin(n)}function en(n){return((n=Math.exp(n))-1/n)/2}function rn(n){return((n=Math.exp(n))+1/n)/2}function un(n){return((n=Math.exp(2*n))-1)/(n+1)}function on(n){return(n=Math.sin(n/2))*n}function an(){}function ln(n,t,e){return this instanceof ln?(this.h=+n,this.s=+t,void(this.l=+e)):arguments.length<2?n instanceof ln?new ln(n.h,n.s,n.l):_n(""+n,wn,ln):new ln(n,t,e)}function cn(n,t,e){function r(n){return n>360?n-=360:0>n&&(n+=360),60>n?u+(o-u)*n/60:180>n?o:240>n?u+(o-u)*(240-n)/60:u}function i(n){return Math.round(255*r(n))}var u,o;return n=isNaN(n)?0:(n%=360)<0?n+360:n,t=isNaN(t)?0:0>t?0:t>1?1:t,e=0>e?0:e>1?1:e,o=.5>=e?e*(1+t):e+t-e*t,u=2*e-o,new mn(i(n+120),i(n),i(n-120))}function fn(n,t,e){return this instanceof fn?(this.h=+n,this.c=+t,void(this.l=+e)):arguments.length<2?n instanceof fn?new fn(n.h,n.c,n.l):n instanceof hn?gn(n.l,n.a,n.b):gn((n=Sn((n=ao.rgb(n)).r,n.g,n.b)).l,n.a,n.b):new fn(n,t,e)}function sn(n,t,e){return isNaN(n)&&(n=0),isNaN(t)&&(t=0),new hn(e,Math.cos(n*=Yo)*t,Math.sin(n)*t)}function hn(n,t,e){return this instanceof hn?(this.l=+n,this.a=+t,void(this.b=+e)):arguments.length<2?n instanceof hn?new hn(n.l,n.a,n.b):n instanceof fn?sn(n.h,n.c,n.l):Sn((n=mn(n)).r,n.g,n.b):new hn(n,t,e)}function pn(n,t,e){var r=(n+16)/116,i=r+t/500,u=r-e/200;return i=vn(i)*na,r=vn(r)*ta,u=vn(u)*ea,new mn(yn(3.2404542*i-1.5371385*r-.4985314*u),yn(-.969266*i+1.8760108*r+.041556*u),yn(.0556434*i-.2040259*r+1.0572252*u))}function gn(n,t,e){return n>0?new fn(Math.atan2(e,t)*Zo,Math.sqrt(t*t+e*e),n):new fn(NaN,NaN,n)}function vn(n){return n>.206893034?n*n*n:(n-4/29)/7.787037}function dn(n){return n>.008856?Math.pow(n,1/3):7.787037*n+4/29}function yn(n){return Math.round(255*(.00304>=n?12.92*n:1.055*Math.pow(n,1/2.4)-.055))}function mn(n,t,e){return this instanceof mn?(this.r=~~n,this.g=~~t,void(this.b=~~e)):arguments.length<2?n instanceof mn?new mn(n.r,n.g,n.b):_n(""+n,mn,cn):new mn(n,t,e)}function Mn(n){return new mn(n>>16,n>>8&255,255&n)}function xn(n){return Mn(n)+""}function bn(n){return 16>n?"0"+Math.max(0,n).toString(16):Math.min(255,n).toString(16)}function _n(n,t,e){var r,i,u,o=0,a=0,l=0;if(r=/([a-z]+)\((.*)\)/.exec(n=n.toLowerCase()))switch(i=r[2].split(","),r[1]){case"hsl":return e(parseFloat(i[0]),parseFloat(i[1])/100,parseFloat(i[2])/100);case"rgb":return t(Nn(i[0]),Nn(i[1]),Nn(i[2]))}return(u=ua.get(n))?t(u.r,u.g,u.b):(null==n||"#"!==n.charAt(0)||isNaN(u=parseInt(n.slice(1),16))||(4===n.length?(o=(3840&u)>>4,o=o>>4|o,a=240&u,a=a>>4|a,l=15&u,l=l<<4|l):7===n.length&&(o=(16711680&u)>>16,a=(65280&u)>>8,l=255&u)),t(o,a,l))}function wn(n,t,e){var r,i,u=Math.min(n/=255,t/=255,e/=255),o=Math.max(n,t,e),a=o-u,l=(o+u)/2;return a?(i=.5>l?a/(o+u):a/(2-o-u),r=n==o?(t-e)/a+(e>t?6:0):t==o?(e-n)/a+2:(n-t)/a+4,r*=60):(r=NaN,i=l>0&&1>l?0:r),new ln(r,i,l)}function Sn(n,t,e){n=kn(n),t=kn(t),e=kn(e);var r=dn((.4124564*n+.3575761*t+.1804375*e)/na),i=dn((.2126729*n+.7151522*t+.072175*e)/ta),u=dn((.0193339*n+.119192*t+.9503041*e)/ea);return hn(116*i-16,500*(r-i),200*(i-u))}function kn(n){return(n/=255)<=.04045?n/12.92:Math.pow((n+.055)/1.055,2.4)}function Nn(n){var t=parseFloat(n);return"%"===n.charAt(n.length-1)?Math.round(2.55*t):t}function En(n){return"function"==typeof n?n:function(){return n}}function An(n){return function(t,e,r){return 2===arguments.length&&"function"==typeof e&&(r=e,e=null),Cn(t,e,n,r)}}function Cn(n,t,e,r){function i(){var n,t=l.status;if(!t&&Ln(l)||t>=200&&300>t||304===t){try{n=e.call(u,l)}catch(r){return void o.error.call(u,r)}o.load.call(u,n)}else o.error.call(u,l)}var u={},o=ao.dispatch("beforesend","progress","load","error"),a={},l=new XMLHttpRequest,c=null;return!this.XDomainRequest||"withCredentials"in l||!/^(http(s)?:)?\/\//.test(n)||(l=new XDomainRequest),"onload"in l?l.onload=l.onerror=i:l.onreadystatechange=function(){l.readyState>3&&i()},l.onprogress=function(n){var t=ao.event;ao.event=n;try{o.progress.call(u,l)}finally{ao.event=t}},u.header=function(n,t){return n=(n+"").toLowerCase(),arguments.length<2?a[n]:(null==t?delete a[n]:a[n]=t+"",u)},u.mimeType=function(n){return arguments.length?(t=null==n?null:n+"",u):t},u.responseType=function(n){return arguments.length?(c=n,u):c},u.response=function(n){return e=n,u},["get","post"].forEach(function(n){u[n]=function(){return u.send.apply(u,[n].concat(co(arguments)))}}),u.send=function(e,r,i){if(2===arguments.length&&"function"==typeof r&&(i=r,r=null),l.open(e,n,!0),null==t||"accept"in a||(a.accept=t+",*/*"),l.setRequestHeader)for(var f in a)l.setRequestHeader(f,a[f]);return null!=t&&l.overrideMimeType&&l.overrideMimeType(t),null!=c&&(l.responseType=c),null!=i&&u.on("error",i).on("load",function(n){i(null,n)}),o.beforesend.call(u,l),l.send(null==r?null:r),u},u.abort=function(){return l.abort(),u},ao.rebind(u,o,"on"),null==r?u:u.get(zn(r))}function zn(n){return 1===n.length?function(t,e){n(null==t?e:null)}:n}function Ln(n){var t=n.responseType;return t&&"text"!==t?n.response:n.responseText}function qn(n,t,e){var r=arguments.length;2>r&&(t=0),3>r&&(e=Date.now());var i=e+t,u={c:n,t:i,n:null};return aa?aa.n=u:oa=u,aa=u,la||(ca=clearTimeout(ca),la=1,fa(Tn)),u}function Tn(){var n=Rn(),t=Dn()-n;t>24?(isFinite(t)&&(clearTimeout(ca),ca=setTimeout(Tn,t)),la=0):(la=1,fa(Tn))}function Rn(){for(var n=Date.now(),t=oa;t;)n>=t.t&&t.c(n-t.t)&&(t.c=null),t=t.n;return n}function Dn(){for(var n,t=oa,e=1/0;t;)t.c?(t.t8?function(n){return n/e}:function(n){return n*e},symbol:n}}function jn(n){var t=n.decimal,e=n.thousands,r=n.grouping,i=n.currency,u=r&&e?function(n,t){for(var i=n.length,u=[],o=0,a=r[0],l=0;i>0&&a>0&&(l+a+1>t&&(a=Math.max(1,t-l)),u.push(n.substring(i-=a,i+a)),!((l+=a+1)>t));)a=r[o=(o+1)%r.length];return u.reverse().join(e)}:m;return function(n){var e=ha.exec(n),r=e[1]||" ",o=e[2]||">",a=e[3]||"-",l=e[4]||"",c=e[5],f=+e[6],s=e[7],h=e[8],p=e[9],g=1,v="",d="",y=!1,m=!0;switch(h&&(h=+h.substring(1)),(c||"0"===r&&"="===o)&&(c=r="0",o="="),p){case"n":s=!0,p="g";break;case"%":g=100,d="%",p="f";break;case"p":g=100,d="%",p="r";break;case"b":case"o":case"x":case"X":"#"===l&&(v="0"+p.toLowerCase());case"c":m=!1;case"d":y=!0,h=0;break;case"s":g=-1,p="r"}"$"===l&&(v=i[0],d=i[1]),"r"!=p||h||(p="g"),null!=h&&("g"==p?h=Math.max(1,Math.min(21,h)):"e"!=p&&"f"!=p||(h=Math.max(0,Math.min(20,h)))),p=pa.get(p)||Fn;var M=c&&s;return function(n){var e=d;if(y&&n%1)return"";var i=0>n||0===n&&0>1/n?(n=-n,"-"):"-"===a?"":a;if(0>g){var l=ao.formatPrefix(n,h);n=l.scale(n),e=l.symbol+d}else n*=g;n=p(n,h);var x,b,_=n.lastIndexOf(".");if(0>_){var w=m?n.lastIndexOf("e"):-1;0>w?(x=n,b=""):(x=n.substring(0,w),b=n.substring(w))}else x=n.substring(0,_),b=t+n.substring(_+1);!c&&s&&(x=u(x,1/0));var S=v.length+x.length+b.length+(M?0:i.length),k=f>S?new Array(S=f-S+1).join(r):"";return M&&(x=u(k+x,k.length?f-b.length:1/0)),i+=v,n=x+b,("<"===o?i+n+k:">"===o?k+i+n:"^"===o?k.substring(0,S>>=1)+i+n+k.substring(S):i+(M?n:k+n))+e}}}function Fn(n){return n+""}function Hn(){this._=new Date(arguments.length>1?Date.UTC.apply(this,arguments):arguments[0])}function On(n,t,e){function r(t){var e=n(t),r=u(e,1);return r-t>t-e?e:r}function i(e){return t(e=n(new va(e-1)),1),e}function u(n,e){return t(n=new va(+n),e),n}function o(n,r,u){var o=i(n),a=[];if(u>1)for(;r>o;)e(o)%u||a.push(new Date(+o)),t(o,1);else for(;r>o;)a.push(new Date(+o)),t(o,1);return a}function a(n,t,e){try{va=Hn;var r=new Hn;return r._=n,o(r,t,e)}finally{va=Date}}n.floor=n,n.round=r,n.ceil=i,n.offset=u,n.range=o;var l=n.utc=In(n);return l.floor=l,l.round=In(r),l.ceil=In(i),l.offset=In(u),l.range=a,n}function In(n){return function(t,e){try{va=Hn;var r=new Hn;return r._=t,n(r,e)._}finally{va=Date}}}function Yn(n){function t(n){function t(t){for(var e,i,u,o=[],a=-1,l=0;++aa;){if(r>=c)return-1;if(i=t.charCodeAt(a++),37===i){if(o=t.charAt(a++),u=C[o in ya?t.charAt(a++):o],!u||(r=u(n,e,r))<0)return-1}else if(i!=e.charCodeAt(r++))return-1}return r}function r(n,t,e){_.lastIndex=0;var r=_.exec(t.slice(e));return r?(n.w=w.get(r[0].toLowerCase()),e+r[0].length):-1}function i(n,t,e){x.lastIndex=0;var r=x.exec(t.slice(e));return r?(n.w=b.get(r[0].toLowerCase()),e+r[0].length):-1}function u(n,t,e){N.lastIndex=0;var r=N.exec(t.slice(e));return r?(n.m=E.get(r[0].toLowerCase()),e+r[0].length):-1}function o(n,t,e){S.lastIndex=0;var r=S.exec(t.slice(e));return r?(n.m=k.get(r[0].toLowerCase()),e+r[0].length):-1}function a(n,t,r){return e(n,A.c.toString(),t,r)}function l(n,t,r){return e(n,A.x.toString(),t,r)}function c(n,t,r){return e(n,A.X.toString(),t,r)}function f(n,t,e){var r=M.get(t.slice(e,e+=2).toLowerCase());return null==r?-1:(n.p=r,e)}var s=n.dateTime,h=n.date,p=n.time,g=n.periods,v=n.days,d=n.shortDays,y=n.months,m=n.shortMonths;t.utc=function(n){function e(n){try{va=Hn;var t=new va;return t._=n,r(t)}finally{va=Date}}var r=t(n);return e.parse=function(n){try{va=Hn;var t=r.parse(n);return t&&t._}finally{va=Date}},e.toString=r.toString,e},t.multi=t.utc.multi=ct;var M=ao.map(),x=Vn(v),b=Xn(v),_=Vn(d),w=Xn(d),S=Vn(y),k=Xn(y),N=Vn(m),E=Xn(m);g.forEach(function(n,t){M.set(n.toLowerCase(),t)});var A={a:function(n){return d[n.getDay()]},A:function(n){return v[n.getDay()]},b:function(n){return m[n.getMonth()]},B:function(n){return y[n.getMonth()]},c:t(s),d:function(n,t){return Zn(n.getDate(),t,2)},e:function(n,t){return Zn(n.getDate(),t,2)},H:function(n,t){return Zn(n.getHours(),t,2)},I:function(n,t){return Zn(n.getHours()%12||12,t,2)},j:function(n,t){return Zn(1+ga.dayOfYear(n),t,3)},L:function(n,t){return Zn(n.getMilliseconds(),t,3)},m:function(n,t){return Zn(n.getMonth()+1,t,2)},M:function(n,t){return Zn(n.getMinutes(),t,2)},p:function(n){return g[+(n.getHours()>=12)]},S:function(n,t){return Zn(n.getSeconds(),t,2)},U:function(n,t){return Zn(ga.sundayOfYear(n),t,2)},w:function(n){return n.getDay()},W:function(n,t){return Zn(ga.mondayOfYear(n),t,2)},x:t(h),X:t(p),y:function(n,t){return Zn(n.getFullYear()%100,t,2)},Y:function(n,t){return Zn(n.getFullYear()%1e4,t,4)},Z:at,"%":function(){return"%"}},C={a:r,A:i,b:u,B:o,c:a,d:tt,e:tt,H:rt,I:rt,j:et,L:ot,m:nt,M:it,p:f,S:ut,U:Bn,w:$n,W:Wn,x:l,X:c,y:Gn,Y:Jn,Z:Kn,"%":lt};return t}function Zn(n,t,e){var r=0>n?"-":"",i=(r?-n:n)+"",u=i.length;return r+(e>u?new Array(e-u+1).join(t)+i:i)}function Vn(n){return new RegExp("^(?:"+n.map(ao.requote).join("|")+")","i")}function Xn(n){for(var t=new c,e=-1,r=n.length;++e68?1900:2e3)}function nt(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+2));return r?(n.m=r[0]-1,e+r[0].length):-1}function tt(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+2));return r?(n.d=+r[0],e+r[0].length):-1}function et(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+3));return r?(n.j=+r[0],e+r[0].length):-1}function rt(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+2));return r?(n.H=+r[0],e+r[0].length):-1}function it(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+2));return r?(n.M=+r[0],e+r[0].length):-1}function ut(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+2));return r?(n.S=+r[0],e+r[0].length):-1}function ot(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+3));return r?(n.L=+r[0],e+r[0].length):-1}function at(n){var t=n.getTimezoneOffset(),e=t>0?"-":"+",r=xo(t)/60|0,i=xo(t)%60;return e+Zn(r,"0",2)+Zn(i,"0",2)}function lt(n,t,e){Ma.lastIndex=0;var r=Ma.exec(t.slice(e,e+1));return r?e+r[0].length:-1}function ct(n){for(var t=n.length,e=-1;++e=0?1:-1,a=o*e,l=Math.cos(t),c=Math.sin(t),f=u*c,s=i*l+f*Math.cos(a),h=f*o*Math.sin(a);ka.add(Math.atan2(h,s)),r=n,i=l,u=c}var t,e,r,i,u;Na.point=function(o,a){Na.point=n,r=(t=o)*Yo,i=Math.cos(a=(e=a)*Yo/2+Fo/4),u=Math.sin(a)},Na.lineEnd=function(){n(t,e)}}function dt(n){var t=n[0],e=n[1],r=Math.cos(e);return[r*Math.cos(t),r*Math.sin(t),Math.sin(e)]}function yt(n,t){return n[0]*t[0]+n[1]*t[1]+n[2]*t[2]}function mt(n,t){return[n[1]*t[2]-n[2]*t[1],n[2]*t[0]-n[0]*t[2],n[0]*t[1]-n[1]*t[0]]}function Mt(n,t){n[0]+=t[0],n[1]+=t[1],n[2]+=t[2]}function xt(n,t){return[n[0]*t,n[1]*t,n[2]*t]}function bt(n){var t=Math.sqrt(n[0]*n[0]+n[1]*n[1]+n[2]*n[2]);n[0]/=t,n[1]/=t,n[2]/=t}function _t(n){return[Math.atan2(n[1],n[0]),tn(n[2])]}function wt(n,t){return xo(n[0]-t[0])a;++a)i.point((e=n[a])[0],e[1]);return void i.lineEnd()}var l=new Tt(e,n,null,!0),c=new Tt(e,null,l,!1);l.o=c,u.push(l),o.push(c),l=new Tt(r,n,null,!1),c=new Tt(r,null,l,!0),l.o=c,u.push(l),o.push(c)}}),o.sort(t),qt(u),qt(o),u.length){for(var a=0,l=e,c=o.length;c>a;++a)o[a].e=l=!l;for(var f,s,h=u[0];;){for(var p=h,g=!0;p.v;)if((p=p.n)===h)return;f=p.z,i.lineStart();do{if(p.v=p.o.v=!0,p.e){if(g)for(var a=0,c=f.length;c>a;++a)i.point((s=f[a])[0],s[1]);else r(p.x,p.n.x,1,i);p=p.n}else{if(g){f=p.p.z;for(var a=f.length-1;a>=0;--a)i.point((s=f[a])[0],s[1])}else r(p.x,p.p.x,-1,i);p=p.p}p=p.o,f=p.z,g=!g}while(!p.v);i.lineEnd()}}}function qt(n){if(t=n.length){for(var t,e,r=0,i=n[0];++r0){for(b||(u.polygonStart(),b=!0),u.lineStart();++o1&&2&t&&e.push(e.pop().concat(e.shift())),p.push(e.filter(Dt))}var p,g,v,d=t(u),y=i.invert(r[0],r[1]),m={point:o,lineStart:l,lineEnd:c,polygonStart:function(){m.point=f,m.lineStart=s,m.lineEnd=h,p=[],g=[]},polygonEnd:function(){m.point=o,m.lineStart=l,m.lineEnd=c,p=ao.merge(p);var n=Ot(y,g);p.length?(b||(u.polygonStart(),b=!0),Lt(p,Ut,n,e,u)):n&&(b||(u.polygonStart(),b=!0),u.lineStart(),e(null,null,1,u),u.lineEnd()),b&&(u.polygonEnd(),b=!1),p=g=null},sphere:function(){u.polygonStart(),u.lineStart(),e(null,null,1,u),u.lineEnd(),u.polygonEnd()}},M=Pt(),x=t(M),b=!1;return m}}function Dt(n){return n.length>1}function Pt(){var n,t=[];return{lineStart:function(){t.push(n=[])},point:function(t,e){n.push([t,e])},lineEnd:b,buffer:function(){var e=t;return t=[],n=null,e},rejoin:function(){t.length>1&&t.push(t.pop().concat(t.shift()))}}}function Ut(n,t){return((n=n.x)[0]<0?n[1]-Io-Uo:Io-n[1])-((t=t.x)[0]<0?t[1]-Io-Uo:Io-t[1])}function jt(n){var t,e=NaN,r=NaN,i=NaN;return{lineStart:function(){n.lineStart(),t=1},point:function(u,o){var a=u>0?Fo:-Fo,l=xo(u-e);xo(l-Fo)0?Io:-Io),n.point(i,r),n.lineEnd(),n.lineStart(),n.point(a,r),n.point(u,r),t=0):i!==a&&l>=Fo&&(xo(e-i)Uo?Math.atan((Math.sin(t)*(u=Math.cos(r))*Math.sin(e)-Math.sin(r)*(i=Math.cos(t))*Math.sin(n))/(i*u*o)):(t+r)/2}function Ht(n,t,e,r){var i;if(null==n)i=e*Io,r.point(-Fo,i),r.point(0,i),r.point(Fo,i),r.point(Fo,0),r.point(Fo,-i),r.point(0,-i),r.point(-Fo,-i),r.point(-Fo,0),r.point(-Fo,i);else if(xo(n[0]-t[0])>Uo){var u=n[0]a;++a){var c=t[a],f=c.length;if(f)for(var s=c[0],h=s[0],p=s[1]/2+Fo/4,g=Math.sin(p),v=Math.cos(p),d=1;;){d===f&&(d=0),n=c[d];var y=n[0],m=n[1]/2+Fo/4,M=Math.sin(m),x=Math.cos(m),b=y-h,_=b>=0?1:-1,w=_*b,S=w>Fo,k=g*M;if(ka.add(Math.atan2(k*_*Math.sin(w),v*x+k*Math.cos(w))),u+=S?b+_*Ho:b,S^h>=e^y>=e){var N=mt(dt(s),dt(n));bt(N);var E=mt(i,N);bt(E);var A=(S^b>=0?-1:1)*tn(E[2]);(r>A||r===A&&(N[0]||N[1]))&&(o+=S^b>=0?1:-1)}if(!d++)break;h=y,g=M,v=x,s=n}}return(-Uo>u||Uo>u&&-Uo>ka)^1&o}function It(n){function t(n,t){return Math.cos(n)*Math.cos(t)>u}function e(n){var e,u,l,c,f;return{lineStart:function(){c=l=!1,f=1},point:function(s,h){var p,g=[s,h],v=t(s,h),d=o?v?0:i(s,h):v?i(s+(0>s?Fo:-Fo),h):0;if(!e&&(c=l=v)&&n.lineStart(),v!==l&&(p=r(e,g),(wt(e,p)||wt(g,p))&&(g[0]+=Uo,g[1]+=Uo,v=t(g[0],g[1]))),v!==l)f=0,v?(n.lineStart(),p=r(g,e),n.point(p[0],p[1])):(p=r(e,g),n.point(p[0],p[1]),n.lineEnd()),e=p;else if(a&&e&&o^v){var y;d&u||!(y=r(g,e,!0))||(f=0,o?(n.lineStart(),n.point(y[0][0],y[0][1]),n.point(y[1][0],y[1][1]),n.lineEnd()):(n.point(y[1][0],y[1][1]),n.lineEnd(),n.lineStart(),n.point(y[0][0],y[0][1])))}!v||e&&wt(e,g)||n.point(g[0],g[1]),e=g,l=v,u=d},lineEnd:function(){l&&n.lineEnd(),e=null},clean:function(){return f|(c&&l)<<1}}}function r(n,t,e){var r=dt(n),i=dt(t),o=[1,0,0],a=mt(r,i),l=yt(a,a),c=a[0],f=l-c*c;if(!f)return!e&&n;var s=u*l/f,h=-u*c/f,p=mt(o,a),g=xt(o,s),v=xt(a,h);Mt(g,v);var d=p,y=yt(g,d),m=yt(d,d),M=y*y-m*(yt(g,g)-1);if(!(0>M)){var x=Math.sqrt(M),b=xt(d,(-y-x)/m);if(Mt(b,g),b=_t(b),!e)return b;var _,w=n[0],S=t[0],k=n[1],N=t[1];w>S&&(_=w,w=S,S=_);var E=S-w,A=xo(E-Fo)E;if(!A&&k>N&&(_=k,k=N,N=_),C?A?k+N>0^b[1]<(xo(b[0]-w)Fo^(w<=b[0]&&b[0]<=S)){var z=xt(d,(-y+x)/m);return Mt(z,g),[b,_t(z)]}}}function i(t,e){var r=o?n:Fo-n,i=0;return-r>t?i|=1:t>r&&(i|=2),-r>e?i|=4:e>r&&(i|=8),i}var u=Math.cos(n),o=u>0,a=xo(u)>Uo,l=ve(n,6*Yo);return Rt(t,e,l,o?[0,-n]:[-Fo,n-Fo])}function Yt(n,t,e,r){return function(i){var u,o=i.a,a=i.b,l=o.x,c=o.y,f=a.x,s=a.y,h=0,p=1,g=f-l,v=s-c;if(u=n-l,g||!(u>0)){if(u/=g,0>g){if(h>u)return;p>u&&(p=u)}else if(g>0){if(u>p)return;u>h&&(h=u)}if(u=e-l,g||!(0>u)){if(u/=g,0>g){if(u>p)return;u>h&&(h=u)}else if(g>0){if(h>u)return;p>u&&(p=u)}if(u=t-c,v||!(u>0)){if(u/=v,0>v){if(h>u)return;p>u&&(p=u)}else if(v>0){if(u>p)return;u>h&&(h=u)}if(u=r-c,v||!(0>u)){if(u/=v,0>v){if(u>p)return;u>h&&(h=u)}else if(v>0){if(h>u)return;p>u&&(p=u)}return h>0&&(i.a={x:l+h*g,y:c+h*v}),1>p&&(i.b={x:l+p*g,y:c+p*v}),i}}}}}}function Zt(n,t,e,r){function i(r,i){return xo(r[0]-n)0?0:3:xo(r[0]-e)0?2:1:xo(r[1]-t)0?1:0:i>0?3:2}function u(n,t){return o(n.x,t.x)}function o(n,t){var e=i(n,1),r=i(t,1);return e!==r?e-r:0===e?t[1]-n[1]:1===e?n[0]-t[0]:2===e?n[1]-t[1]:t[0]-n[0]}return function(a){function l(n){for(var t=0,e=d.length,r=n[1],i=0;e>i;++i)for(var u,o=1,a=d[i],l=a.length,c=a[0];l>o;++o)u=a[o],c[1]<=r?u[1]>r&&Q(c,u,n)>0&&++t:u[1]<=r&&Q(c,u,n)<0&&--t,c=u;return 0!==t}function c(u,a,l,c){var f=0,s=0;if(null==u||(f=i(u,l))!==(s=i(a,l))||o(u,a)<0^l>0){do c.point(0===f||3===f?n:e,f>1?r:t);while((f=(f+l+4)%4)!==s)}else c.point(a[0],a[1])}function f(i,u){return i>=n&&e>=i&&u>=t&&r>=u}function s(n,t){f(n,t)&&a.point(n,t)}function h(){C.point=g,d&&d.push(y=[]),S=!0,w=!1,b=_=NaN}function p(){v&&(g(m,M),x&&w&&E.rejoin(),v.push(E.buffer())),C.point=s,w&&a.lineEnd()}function g(n,t){n=Math.max(-Ha,Math.min(Ha,n)),t=Math.max(-Ha,Math.min(Ha,t));var e=f(n,t);if(d&&y.push([n,t]),S)m=n,M=t,x=e,S=!1,e&&(a.lineStart(),a.point(n,t));else if(e&&w)a.point(n,t);else{var r={a:{x:b,y:_},b:{x:n,y:t}};A(r)?(w||(a.lineStart(),a.point(r.a.x,r.a.y)),a.point(r.b.x,r.b.y),e||a.lineEnd(),k=!1):e&&(a.lineStart(),a.point(n,t),k=!1)}b=n,_=t,w=e}var v,d,y,m,M,x,b,_,w,S,k,N=a,E=Pt(),A=Yt(n,t,e,r),C={point:s,lineStart:h,lineEnd:p,polygonStart:function(){a=E,v=[],d=[],k=!0},polygonEnd:function(){a=N,v=ao.merge(v);var t=l([n,r]),e=k&&t,i=v.length;(e||i)&&(a.polygonStart(),e&&(a.lineStart(),c(null,null,1,a),a.lineEnd()),i&&Lt(v,u,t,c,a),a.polygonEnd()),v=d=y=null}};return C}}function Vt(n){var t=0,e=Fo/3,r=ae(n),i=r(t,e);return i.parallels=function(n){return arguments.length?r(t=n[0]*Fo/180,e=n[1]*Fo/180):[t/Fo*180,e/Fo*180]},i}function Xt(n,t){function e(n,t){var e=Math.sqrt(u-2*i*Math.sin(t))/i;return[e*Math.sin(n*=i),o-e*Math.cos(n)]}var r=Math.sin(n),i=(r+Math.sin(t))/2,u=1+r*(2*i-r),o=Math.sqrt(u)/i;return e.invert=function(n,t){var e=o-t;return[Math.atan2(n,e)/i,tn((u-(n*n+e*e)*i*i)/(2*i))]},e}function $t(){function n(n,t){Ia+=i*n-r*t,r=n,i=t}var t,e,r,i;$a.point=function(u,o){$a.point=n,t=r=u,e=i=o},$a.lineEnd=function(){n(t,e)}}function Bt(n,t){Ya>n&&(Ya=n),n>Va&&(Va=n),Za>t&&(Za=t),t>Xa&&(Xa=t)}function Wt(){function n(n,t){o.push("M",n,",",t,u)}function t(n,t){o.push("M",n,",",t),a.point=e}function e(n,t){o.push("L",n,",",t)}function r(){a.point=n}function i(){o.push("Z")}var u=Jt(4.5),o=[],a={point:n,lineStart:function(){a.point=t},lineEnd:r,polygonStart:function(){a.lineEnd=i},polygonEnd:function(){a.lineEnd=r,a.point=n},pointRadius:function(n){return u=Jt(n),a},result:function(){if(o.length){var n=o.join("");return o=[],n}}};return a}function Jt(n){return"m0,"+n+"a"+n+","+n+" 0 1,1 0,"+-2*n+"a"+n+","+n+" 0 1,1 0,"+2*n+"z"}function Gt(n,t){Ca+=n,za+=t,++La}function Kt(){function n(n,r){var i=n-t,u=r-e,o=Math.sqrt(i*i+u*u);qa+=o*(t+n)/2,Ta+=o*(e+r)/2,Ra+=o,Gt(t=n,e=r)}var t,e;Wa.point=function(r,i){Wa.point=n,Gt(t=r,e=i)}}function Qt(){Wa.point=Gt}function ne(){function n(n,t){var e=n-r,u=t-i,o=Math.sqrt(e*e+u*u);qa+=o*(r+n)/2,Ta+=o*(i+t)/2,Ra+=o,o=i*n-r*t,Da+=o*(r+n),Pa+=o*(i+t),Ua+=3*o,Gt(r=n,i=t)}var t,e,r,i;Wa.point=function(u,o){Wa.point=n,Gt(t=r=u,e=i=o)},Wa.lineEnd=function(){n(t,e)}}function te(n){function t(t,e){n.moveTo(t+o,e),n.arc(t,e,o,0,Ho)}function e(t,e){n.moveTo(t,e),a.point=r}function r(t,e){n.lineTo(t,e)}function i(){a.point=t}function u(){n.closePath()}var o=4.5,a={point:t,lineStart:function(){a.point=e},lineEnd:i,polygonStart:function(){a.lineEnd=u},polygonEnd:function(){a.lineEnd=i,a.point=t},pointRadius:function(n){return o=n,a},result:b};return a}function ee(n){function t(n){return(a?r:e)(n)}function e(t){return ue(t,function(e,r){e=n(e,r),t.point(e[0],e[1])})}function r(t){function e(e,r){e=n(e,r),t.point(e[0],e[1])}function r(){M=NaN,S.point=u,t.lineStart()}function u(e,r){var u=dt([e,r]),o=n(e,r);i(M,x,m,b,_,w,M=o[0],x=o[1],m=e,b=u[0],_=u[1],w=u[2],a,t),t.point(M,x)}function o(){S.point=e,t.lineEnd()}function l(){
+r(),S.point=c,S.lineEnd=f}function c(n,t){u(s=n,h=t),p=M,g=x,v=b,d=_,y=w,S.point=u}function f(){i(M,x,m,b,_,w,p,g,s,v,d,y,a,t),S.lineEnd=o,o()}var s,h,p,g,v,d,y,m,M,x,b,_,w,S={point:e,lineStart:r,lineEnd:o,polygonStart:function(){t.polygonStart(),S.lineStart=l},polygonEnd:function(){t.polygonEnd(),S.lineStart=r}};return S}function i(t,e,r,a,l,c,f,s,h,p,g,v,d,y){var m=f-t,M=s-e,x=m*m+M*M;if(x>4*u&&d--){var b=a+p,_=l+g,w=c+v,S=Math.sqrt(b*b+_*_+w*w),k=Math.asin(w/=S),N=xo(xo(w)-1)u||xo((m*z+M*L)/x-.5)>.3||o>a*p+l*g+c*v)&&(i(t,e,r,a,l,c,A,C,N,b/=S,_/=S,w,d,y),y.point(A,C),i(A,C,N,b,_,w,f,s,h,p,g,v,d,y))}}var u=.5,o=Math.cos(30*Yo),a=16;return t.precision=function(n){return arguments.length?(a=(u=n*n)>0&&16,t):Math.sqrt(u)},t}function re(n){var t=ee(function(t,e){return n([t*Zo,e*Zo])});return function(n){return le(t(n))}}function ie(n){this.stream=n}function ue(n,t){return{point:t,sphere:function(){n.sphere()},lineStart:function(){n.lineStart()},lineEnd:function(){n.lineEnd()},polygonStart:function(){n.polygonStart()},polygonEnd:function(){n.polygonEnd()}}}function oe(n){return ae(function(){return n})()}function ae(n){function t(n){return n=a(n[0]*Yo,n[1]*Yo),[n[0]*h+l,c-n[1]*h]}function e(n){return n=a.invert((n[0]-l)/h,(c-n[1])/h),n&&[n[0]*Zo,n[1]*Zo]}function r(){a=Ct(o=se(y,M,x),u);var n=u(v,d);return l=p-n[0]*h,c=g+n[1]*h,i()}function i(){return f&&(f.valid=!1,f=null),t}var u,o,a,l,c,f,s=ee(function(n,t){return n=u(n,t),[n[0]*h+l,c-n[1]*h]}),h=150,p=480,g=250,v=0,d=0,y=0,M=0,x=0,b=Fa,_=m,w=null,S=null;return t.stream=function(n){return f&&(f.valid=!1),f=le(b(o,s(_(n)))),f.valid=!0,f},t.clipAngle=function(n){return arguments.length?(b=null==n?(w=n,Fa):It((w=+n)*Yo),i()):w},t.clipExtent=function(n){return arguments.length?(S=n,_=n?Zt(n[0][0],n[0][1],n[1][0],n[1][1]):m,i()):S},t.scale=function(n){return arguments.length?(h=+n,r()):h},t.translate=function(n){return arguments.length?(p=+n[0],g=+n[1],r()):[p,g]},t.center=function(n){return arguments.length?(v=n[0]%360*Yo,d=n[1]%360*Yo,r()):[v*Zo,d*Zo]},t.rotate=function(n){return arguments.length?(y=n[0]%360*Yo,M=n[1]%360*Yo,x=n.length>2?n[2]%360*Yo:0,r()):[y*Zo,M*Zo,x*Zo]},ao.rebind(t,s,"precision"),function(){return u=n.apply(this,arguments),t.invert=u.invert&&e,r()}}function le(n){return ue(n,function(t,e){n.point(t*Yo,e*Yo)})}function ce(n,t){return[n,t]}function fe(n,t){return[n>Fo?n-Ho:-Fo>n?n+Ho:n,t]}function se(n,t,e){return n?t||e?Ct(pe(n),ge(t,e)):pe(n):t||e?ge(t,e):fe}function he(n){return function(t,e){return t+=n,[t>Fo?t-Ho:-Fo>t?t+Ho:t,e]}}function pe(n){var t=he(n);return t.invert=he(-n),t}function ge(n,t){function e(n,t){var e=Math.cos(t),a=Math.cos(n)*e,l=Math.sin(n)*e,c=Math.sin(t),f=c*r+a*i;return[Math.atan2(l*u-f*o,a*r-c*i),tn(f*u+l*o)]}var r=Math.cos(n),i=Math.sin(n),u=Math.cos(t),o=Math.sin(t);return e.invert=function(n,t){var e=Math.cos(t),a=Math.cos(n)*e,l=Math.sin(n)*e,c=Math.sin(t),f=c*u-l*o;return[Math.atan2(l*u+c*o,a*r+f*i),tn(f*r-a*i)]},e}function ve(n,t){var e=Math.cos(n),r=Math.sin(n);return function(i,u,o,a){var l=o*t;null!=i?(i=de(e,i),u=de(e,u),(o>0?u>i:i>u)&&(i+=o*Ho)):(i=n+o*Ho,u=n-.5*l);for(var c,f=i;o>0?f>u:u>f;f-=l)a.point((c=_t([e,-r*Math.cos(f),-r*Math.sin(f)]))[0],c[1])}}function de(n,t){var e=dt(t);e[0]-=n,bt(e);var r=nn(-e[1]);return((-e[2]<0?-r:r)+2*Math.PI-Uo)%(2*Math.PI)}function ye(n,t,e){var r=ao.range(n,t-Uo,e).concat(t);return function(n){return r.map(function(t){return[n,t]})}}function me(n,t,e){var r=ao.range(n,t-Uo,e).concat(t);return function(n){return r.map(function(t){return[t,n]})}}function Me(n){return n.source}function xe(n){return n.target}function be(n,t,e,r){var i=Math.cos(t),u=Math.sin(t),o=Math.cos(r),a=Math.sin(r),l=i*Math.cos(n),c=i*Math.sin(n),f=o*Math.cos(e),s=o*Math.sin(e),h=2*Math.asin(Math.sqrt(on(r-t)+i*o*on(e-n))),p=1/Math.sin(h),g=h?function(n){var t=Math.sin(n*=h)*p,e=Math.sin(h-n)*p,r=e*l+t*f,i=e*c+t*s,o=e*u+t*a;return[Math.atan2(i,r)*Zo,Math.atan2(o,Math.sqrt(r*r+i*i))*Zo]}:function(){return[n*Zo,t*Zo]};return g.distance=h,g}function _e(){function n(n,i){var u=Math.sin(i*=Yo),o=Math.cos(i),a=xo((n*=Yo)-t),l=Math.cos(a);Ja+=Math.atan2(Math.sqrt((a=o*Math.sin(a))*a+(a=r*u-e*o*l)*a),e*u+r*o*l),t=n,e=u,r=o}var t,e,r;Ga.point=function(i,u){t=i*Yo,e=Math.sin(u*=Yo),r=Math.cos(u),Ga.point=n},Ga.lineEnd=function(){Ga.point=Ga.lineEnd=b}}function we(n,t){function e(t,e){var r=Math.cos(t),i=Math.cos(e),u=n(r*i);return[u*i*Math.sin(t),u*Math.sin(e)]}return e.invert=function(n,e){var r=Math.sqrt(n*n+e*e),i=t(r),u=Math.sin(i),o=Math.cos(i);return[Math.atan2(n*u,r*o),Math.asin(r&&e*u/r)]},e}function Se(n,t){function e(n,t){o>0?-Io+Uo>t&&(t=-Io+Uo):t>Io-Uo&&(t=Io-Uo);var e=o/Math.pow(i(t),u);return[e*Math.sin(u*n),o-e*Math.cos(u*n)]}var r=Math.cos(n),i=function(n){return Math.tan(Fo/4+n/2)},u=n===t?Math.sin(n):Math.log(r/Math.cos(t))/Math.log(i(t)/i(n)),o=r*Math.pow(i(n),u)/u;return u?(e.invert=function(n,t){var e=o-t,r=K(u)*Math.sqrt(n*n+e*e);return[Math.atan2(n,e)/u,2*Math.atan(Math.pow(o/r,1/u))-Io]},e):Ne}function ke(n,t){function e(n,t){var e=u-t;return[e*Math.sin(i*n),u-e*Math.cos(i*n)]}var r=Math.cos(n),i=n===t?Math.sin(n):(r-Math.cos(t))/(t-n),u=r/i+n;return xo(i)i;i++){for(;r>1&&Q(n[e[r-2]],n[e[r-1]],n[i])<=0;)--r;e[r++]=i}return e.slice(0,r)}function qe(n,t){return n[0]-t[0]||n[1]-t[1]}function Te(n,t,e){return(e[0]-t[0])*(n[1]-t[1])<(e[1]-t[1])*(n[0]-t[0])}function Re(n,t,e,r){var i=n[0],u=e[0],o=t[0]-i,a=r[0]-u,l=n[1],c=e[1],f=t[1]-l,s=r[1]-c,h=(a*(l-c)-s*(i-u))/(s*o-a*f);return[i+h*o,l+h*f]}function De(n){var t=n[0],e=n[n.length-1];return!(t[0]-e[0]||t[1]-e[1])}function Pe(){rr(this),this.edge=this.site=this.circle=null}function Ue(n){var t=cl.pop()||new Pe;return t.site=n,t}function je(n){Be(n),ol.remove(n),cl.push(n),rr(n)}function Fe(n){var t=n.circle,e=t.x,r=t.cy,i={x:e,y:r},u=n.P,o=n.N,a=[n];je(n);for(var l=u;l.circle&&xo(e-l.circle.x)f;++f)c=a[f],l=a[f-1],nr(c.edge,l.site,c.site,i);l=a[0],c=a[s-1],c.edge=Ke(l.site,c.site,null,i),$e(l),$e(c)}function He(n){for(var t,e,r,i,u=n.x,o=n.y,a=ol._;a;)if(r=Oe(a,o)-u,r>Uo)a=a.L;else{if(i=u-Ie(a,o),!(i>Uo)){r>-Uo?(t=a.P,e=a):i>-Uo?(t=a,e=a.N):t=e=a;break}if(!a.R){t=a;break}a=a.R}var l=Ue(n);if(ol.insert(t,l),t||e){if(t===e)return Be(t),e=Ue(t.site),ol.insert(l,e),l.edge=e.edge=Ke(t.site,l.site),$e(t),void $e(e);if(!e)return void(l.edge=Ke(t.site,l.site));Be(t),Be(e);var c=t.site,f=c.x,s=c.y,h=n.x-f,p=n.y-s,g=e.site,v=g.x-f,d=g.y-s,y=2*(h*d-p*v),m=h*h+p*p,M=v*v+d*d,x={x:(d*m-p*M)/y+f,y:(h*M-v*m)/y+s};nr(e.edge,c,g,x),l.edge=Ke(c,n,null,x),e.edge=Ke(n,g,null,x),$e(t),$e(e)}}function Oe(n,t){var e=n.site,r=e.x,i=e.y,u=i-t;if(!u)return r;var o=n.P;if(!o)return-(1/0);e=o.site;var a=e.x,l=e.y,c=l-t;if(!c)return a;var f=a-r,s=1/u-1/c,h=f/c;return s?(-h+Math.sqrt(h*h-2*s*(f*f/(-2*c)-l+c/2+i-u/2)))/s+r:(r+a)/2}function Ie(n,t){var e=n.N;if(e)return Oe(e,t);var r=n.site;return r.y===t?r.x:1/0}function Ye(n){this.site=n,this.edges=[]}function Ze(n){for(var t,e,r,i,u,o,a,l,c,f,s=n[0][0],h=n[1][0],p=n[0][1],g=n[1][1],v=ul,d=v.length;d--;)if(u=v[d],u&&u.prepare())for(a=u.edges,l=a.length,o=0;l>o;)f=a[o].end(),r=f.x,i=f.y,c=a[++o%l].start(),t=c.x,e=c.y,(xo(r-t)>Uo||xo(i-e)>Uo)&&(a.splice(o,0,new tr(Qe(u.site,f,xo(r-s)Uo?{x:s,y:xo(t-s)Uo?{x:xo(e-g)Uo?{x:h,y:xo(t-h)Uo?{x:xo(e-p)=-jo)){var p=l*l+c*c,g=f*f+s*s,v=(s*p-c*g)/h,d=(l*g-f*p)/h,s=d+a,y=fl.pop()||new Xe;y.arc=n,y.site=i,y.x=v+o,y.y=s+Math.sqrt(v*v+d*d),y.cy=s,n.circle=y;for(var m=null,M=ll._;M;)if(y.yd||d>=a)return;if(h>g){if(u){if(u.y>=c)return}else u={x:d,y:l};e={x:d,y:c}}else{if(u){if(u.yr||r>1)if(h>g){if(u){if(u.y>=c)return}else u={x:(l-i)/r,y:l};e={x:(c-i)/r,y:c}}else{if(u){if(u.yp){if(u){if(u.x>=a)return}else u={x:o,y:r*o+i};e={x:a,y:r*a+i}}else{if(u){if(u.xu||s>o||r>h||i>p)){if(g=n.point){var g,v=t-n.x,d=e-n.y,y=v*v+d*d;if(l>y){var m=Math.sqrt(l=y);r=t-m,i=e-m,u=t+m,o=e+m,a=g}}for(var M=n.nodes,x=.5*(f+h),b=.5*(s+p),_=t>=x,w=e>=b,S=w<<1|_,k=S+4;k>S;++S)if(n=M[3&S])switch(3&S){case 0:c(n,f,s,x,b);break;case 1:c(n,x,s,h,b);break;case 2:c(n,f,b,x,p);break;case 3:c(n,x,b,h,p)}}}(n,r,i,u,o),a}function vr(n,t){n=ao.rgb(n),t=ao.rgb(t);var e=n.r,r=n.g,i=n.b,u=t.r-e,o=t.g-r,a=t.b-i;return function(n){return"#"+bn(Math.round(e+u*n))+bn(Math.round(r+o*n))+bn(Math.round(i+a*n))}}function dr(n,t){var e,r={},i={};for(e in n)e in t?r[e]=Mr(n[e],t[e]):i[e]=n[e];for(e in t)e in n||(i[e]=t[e]);return function(n){for(e in r)i[e]=r[e](n);return i}}function yr(n,t){return n=+n,t=+t,function(e){return n*(1-e)+t*e}}function mr(n,t){var e,r,i,u=hl.lastIndex=pl.lastIndex=0,o=-1,a=[],l=[];for(n+="",t+="";(e=hl.exec(n))&&(r=pl.exec(t));)(i=r.index)>u&&(i=t.slice(u,i),a[o]?a[o]+=i:a[++o]=i),(e=e[0])===(r=r[0])?a[o]?a[o]+=r:a[++o]=r:(a[++o]=null,l.push({i:o,x:yr(e,r)})),u=pl.lastIndex;return ur;++r)a[(e=l[r]).i]=e.x(n);return a.join("")})}function Mr(n,t){for(var e,r=ao.interpolators.length;--r>=0&&!(e=ao.interpolators[r](n,t)););return e}function xr(n,t){var e,r=[],i=[],u=n.length,o=t.length,a=Math.min(n.length,t.length);for(e=0;a>e;++e)r.push(Mr(n[e],t[e]));for(;u>e;++e)i[e]=n[e];for(;o>e;++e)i[e]=t[e];return function(n){for(e=0;a>e;++e)i[e]=r[e](n);return i}}function br(n){return function(t){return 0>=t?0:t>=1?1:n(t)}}function _r(n){return function(t){return 1-n(1-t)}}function wr(n){return function(t){return.5*(.5>t?n(2*t):2-n(2-2*t))}}function Sr(n){return n*n}function kr(n){return n*n*n}function Nr(n){if(0>=n)return 0;if(n>=1)return 1;var t=n*n,e=t*n;return 4*(.5>n?e:3*(n-t)+e-.75)}function Er(n){return function(t){return Math.pow(t,n)}}function Ar(n){return 1-Math.cos(n*Io)}function Cr(n){return Math.pow(2,10*(n-1))}function zr(n){return 1-Math.sqrt(1-n*n)}function Lr(n,t){var e;return arguments.length<2&&(t=.45),arguments.length?e=t/Ho*Math.asin(1/n):(n=1,e=t/4),function(r){return 1+n*Math.pow(2,-10*r)*Math.sin((r-e)*Ho/t)}}function qr(n){return n||(n=1.70158),function(t){return t*t*((n+1)*t-n)}}function Tr(n){return 1/2.75>n?7.5625*n*n:2/2.75>n?7.5625*(n-=1.5/2.75)*n+.75:2.5/2.75>n?7.5625*(n-=2.25/2.75)*n+.9375:7.5625*(n-=2.625/2.75)*n+.984375}function Rr(n,t){n=ao.hcl(n),t=ao.hcl(t);var e=n.h,r=n.c,i=n.l,u=t.h-e,o=t.c-r,a=t.l-i;return isNaN(o)&&(o=0,r=isNaN(r)?t.c:r),isNaN(u)?(u=0,e=isNaN(e)?t.h:e):u>180?u-=360:-180>u&&(u+=360),function(n){return sn(e+u*n,r+o*n,i+a*n)+""}}function Dr(n,t){n=ao.hsl(n),t=ao.hsl(t);var e=n.h,r=n.s,i=n.l,u=t.h-e,o=t.s-r,a=t.l-i;return isNaN(o)&&(o=0,r=isNaN(r)?t.s:r),isNaN(u)?(u=0,e=isNaN(e)?t.h:e):u>180?u-=360:-180>u&&(u+=360),function(n){return cn(e+u*n,r+o*n,i+a*n)+""}}function Pr(n,t){n=ao.lab(n),t=ao.lab(t);var e=n.l,r=n.a,i=n.b,u=t.l-e,o=t.a-r,a=t.b-i;return function(n){return pn(e+u*n,r+o*n,i+a*n)+""}}function Ur(n,t){return t-=n,function(e){return Math.round(n+t*e)}}function jr(n){var t=[n.a,n.b],e=[n.c,n.d],r=Hr(t),i=Fr(t,e),u=Hr(Or(e,t,-i))||0;t[0]*e[1]180?t+=360:t-n>180&&(n+=360),r.push({i:e.push(Ir(e)+"rotate(",null,")")-2,x:yr(n,t)})):t&&e.push(Ir(e)+"rotate("+t+")")}function Vr(n,t,e,r){n!==t?r.push({i:e.push(Ir(e)+"skewX(",null,")")-2,x:yr(n,t)}):t&&e.push(Ir(e)+"skewX("+t+")")}function Xr(n,t,e,r){if(n[0]!==t[0]||n[1]!==t[1]){var i=e.push(Ir(e)+"scale(",null,",",null,")");r.push({i:i-4,x:yr(n[0],t[0])},{i:i-2,x:yr(n[1],t[1])})}else 1===t[0]&&1===t[1]||e.push(Ir(e)+"scale("+t+")")}function $r(n,t){var e=[],r=[];return n=ao.transform(n),t=ao.transform(t),Yr(n.translate,t.translate,e,r),Zr(n.rotate,t.rotate,e,r),Vr(n.skew,t.skew,e,r),Xr(n.scale,t.scale,e,r),n=t=null,function(n){for(var t,i=-1,u=r.length;++i=0;)e.push(i[r])}function oi(n,t){for(var e=[n],r=[];null!=(n=e.pop());)if(r.push(n),(u=n.children)&&(i=u.length))for(var i,u,o=-1;++oe;++e)(t=n[e][1])>i&&(r=e,i=t);return r}function yi(n){return n.reduce(mi,0)}function mi(n,t){return n+t[1]}function Mi(n,t){return xi(n,Math.ceil(Math.log(t.length)/Math.LN2+1))}function xi(n,t){for(var e=-1,r=+n[0],i=(n[1]-r)/t,u=[];++e<=t;)u[e]=i*e+r;return u}function bi(n){return[ao.min(n),ao.max(n)]}function _i(n,t){return n.value-t.value}function wi(n,t){var e=n._pack_next;n._pack_next=t,t._pack_prev=n,t._pack_next=e,e._pack_prev=t}function Si(n,t){n._pack_next=t,t._pack_prev=n}function ki(n,t){var e=t.x-n.x,r=t.y-n.y,i=n.r+t.r;return.999*i*i>e*e+r*r}function Ni(n){function t(n){f=Math.min(n.x-n.r,f),s=Math.max(n.x+n.r,s),h=Math.min(n.y-n.r,h),p=Math.max(n.y+n.r,p)}if((e=n.children)&&(c=e.length)){var e,r,i,u,o,a,l,c,f=1/0,s=-(1/0),h=1/0,p=-(1/0);if(e.forEach(Ei),r=e[0],r.x=-r.r,r.y=0,t(r),c>1&&(i=e[1],i.x=i.r,i.y=0,t(i),c>2))for(u=e[2],zi(r,i,u),t(u),wi(r,u),r._pack_prev=u,wi(u,i),i=r._pack_next,o=3;c>o;o++){zi(r,i,u=e[o]);var g=0,v=1,d=1;for(a=i._pack_next;a!==i;a=a._pack_next,v++)if(ki(a,u)){g=1;break}if(1==g)for(l=r._pack_prev;l!==a._pack_prev&&!ki(l,u);l=l._pack_prev,d++);g?(d>v||v==d&&i.ro;o++)u=e[o],u.x-=y,u.y-=m,M=Math.max(M,u.r+Math.sqrt(u.x*u.x+u.y*u.y));n.r=M,e.forEach(Ai)}}function Ei(n){n._pack_next=n._pack_prev=n}function Ai(n){delete n._pack_next,delete n._pack_prev}function Ci(n,t,e,r){var i=n.children;if(n.x=t+=r*n.x,n.y=e+=r*n.y,n.r*=r,i)for(var u=-1,o=i.length;++u=0;)t=i[u],t.z+=e,t.m+=e,e+=t.s+(r+=t.c)}function Pi(n,t,e){return n.a.parent===t.parent?n.a:e}function Ui(n){return 1+ao.max(n,function(n){return n.y})}function ji(n){return n.reduce(function(n,t){return n+t.x},0)/n.length}function Fi(n){var t=n.children;return t&&t.length?Fi(t[0]):n}function Hi(n){var t,e=n.children;return e&&(t=e.length)?Hi(e[t-1]):n}function Oi(n){return{x:n.x,y:n.y,dx:n.dx,dy:n.dy}}function Ii(n,t){var e=n.x+t[3],r=n.y+t[0],i=n.dx-t[1]-t[3],u=n.dy-t[0]-t[2];return 0>i&&(e+=i/2,i=0),0>u&&(r+=u/2,u=0),{x:e,y:r,dx:i,dy:u}}function Yi(n){var t=n[0],e=n[n.length-1];return e>t?[t,e]:[e,t]}function Zi(n){return n.rangeExtent?n.rangeExtent():Yi(n.range())}function Vi(n,t,e,r){var i=e(n[0],n[1]),u=r(t[0],t[1]);return function(n){return u(i(n))}}function Xi(n,t){var e,r=0,i=n.length-1,u=n[r],o=n[i];return u>o&&(e=r,r=i,i=e,e=u,u=o,o=e),n[r]=t.floor(u),n[i]=t.ceil(o),n}function $i(n){return n?{floor:function(t){return Math.floor(t/n)*n},ceil:function(t){return Math.ceil(t/n)*n}}:Sl}function Bi(n,t,e,r){var i=[],u=[],o=0,a=Math.min(n.length,t.length)-1;for(n[a]2?Bi:Vi,l=r?Wr:Br;return o=i(n,t,l,e),a=i(t,n,l,Mr),u}function u(n){return o(n)}var o,a;return u.invert=function(n){return a(n)},u.domain=function(t){return arguments.length?(n=t.map(Number),i()):n},u.range=function(n){return arguments.length?(t=n,i()):t},u.rangeRound=function(n){return u.range(n).interpolate(Ur)},u.clamp=function(n){return arguments.length?(r=n,i()):r},u.interpolate=function(n){return arguments.length?(e=n,i()):e},u.ticks=function(t){return Qi(n,t)},u.tickFormat=function(t,e){return nu(n,t,e)},u.nice=function(t){return Gi(n,t),i()},u.copy=function(){return Wi(n,t,e,r)},i()}function Ji(n,t){return ao.rebind(n,t,"range","rangeRound","interpolate","clamp")}function Gi(n,t){return Xi(n,$i(Ki(n,t)[2])),Xi(n,$i(Ki(n,t)[2])),n}function Ki(n,t){null==t&&(t=10);var e=Yi(n),r=e[1]-e[0],i=Math.pow(10,Math.floor(Math.log(r/t)/Math.LN10)),u=t/r*i;return.15>=u?i*=10:.35>=u?i*=5:.75>=u&&(i*=2),e[0]=Math.ceil(e[0]/i)*i,e[1]=Math.floor(e[1]/i)*i+.5*i,e[2]=i,e}function Qi(n,t){return ao.range.apply(ao,Ki(n,t))}function nu(n,t,e){var r=Ki(n,t);if(e){var i=ha.exec(e);if(i.shift(),"s"===i[8]){var u=ao.formatPrefix(Math.max(xo(r[0]),xo(r[1])));return i[7]||(i[7]="."+tu(u.scale(r[2]))),i[8]="f",e=ao.format(i.join("")),function(n){return e(u.scale(n))+u.symbol}}i[7]||(i[7]="."+eu(i[8],r)),e=i.join("")}else e=",."+tu(r[2])+"f";return ao.format(e)}function tu(n){return-Math.floor(Math.log(n)/Math.LN10+.01)}function eu(n,t){var e=tu(t[2]);return n in kl?Math.abs(e-tu(Math.max(xo(t[0]),xo(t[1]))))+ +("e"!==n):e-2*("%"===n)}function ru(n,t,e,r){function i(n){return(e?Math.log(0>n?0:n):-Math.log(n>0?0:-n))/Math.log(t)}function u(n){return e?Math.pow(t,n):-Math.pow(t,-n)}function o(t){return n(i(t))}return o.invert=function(t){return u(n.invert(t))},o.domain=function(t){return arguments.length?(e=t[0]>=0,n.domain((r=t.map(Number)).map(i)),o):r},o.base=function(e){return arguments.length?(t=+e,n.domain(r.map(i)),o):t},o.nice=function(){var t=Xi(r.map(i),e?Math:El);return n.domain(t),r=t.map(u),o},o.ticks=function(){var n=Yi(r),o=[],a=n[0],l=n[1],c=Math.floor(i(a)),f=Math.ceil(i(l)),s=t%1?2:t;if(isFinite(f-c)){if(e){for(;f>c;c++)for(var h=1;s>h;h++)o.push(u(c)*h);o.push(u(c))}else for(o.push(u(c));c++0;h--)o.push(u(c)*h);for(c=0;o[c]l;f--);o=o.slice(c,f)}return o},o.tickFormat=function(n,e){if(!arguments.length)return Nl;arguments.length<2?e=Nl:"function"!=typeof e&&(e=ao.format(e));var r=Math.max(1,t*n/o.ticks().length);return function(n){var o=n/u(Math.round(i(n)));return t-.5>o*t&&(o*=t),r>=o?e(n):""}},o.copy=function(){return ru(n.copy(),t,e,r)},Ji(o,n)}function iu(n,t,e){function r(t){return n(i(t))}var i=uu(t),u=uu(1/t);return r.invert=function(t){return u(n.invert(t))},r.domain=function(t){return arguments.length?(n.domain((e=t.map(Number)).map(i)),r):e},r.ticks=function(n){return Qi(e,n)},r.tickFormat=function(n,t){return nu(e,n,t)},r.nice=function(n){return r.domain(Gi(e,n))},r.exponent=function(o){return arguments.length?(i=uu(t=o),u=uu(1/t),n.domain(e.map(i)),r):t},r.copy=function(){return iu(n.copy(),t,e)},Ji(r,n)}function uu(n){return function(t){return 0>t?-Math.pow(-t,n):Math.pow(t,n)}}function ou(n,t){function e(e){return u[((i.get(e)||("range"===t.t?i.set(e,n.push(e)):NaN))-1)%u.length]}function r(t,e){return ao.range(n.length).map(function(n){return t+e*n})}var i,u,o;return e.domain=function(r){if(!arguments.length)return n;n=[],i=new c;for(var u,o=-1,a=r.length;++o e?[NaN,NaN]:[e>0?a[e-1]:n[0],e t?NaN:t/u+n,[t,t+1/u]},r.copy=function(){return lu(n,t,e)},i()}function cu(n,t){function e(e){return e>=e?t[ao.bisect(n,e)]:void 0}return e.domain=function(t){return arguments.length?(n=t,e):n},e.range=function(n){return arguments.length?(t=n,e):t},e.invertExtent=function(e){return e=t.indexOf(e),[n[e-1],n[e]]},e.copy=function(){return cu(n,t)},e}function fu(n){function t(n){return+n}return t.invert=t,t.domain=t.range=function(e){return arguments.length?(n=e.map(t),t):n},t.ticks=function(t){return Qi(n,t)},t.tickFormat=function(t,e){return nu(n,t,e)},t.copy=function(){return fu(n)},t}function su(){return 0}function hu(n){return n.innerRadius}function pu(n){return n.outerRadius}function gu(n){return n.startAngle}function vu(n){return n.endAngle}function du(n){return n&&n.padAngle}function yu(n,t,e,r){return(n-e)*t-(t-r)*n>0?0:1}function mu(n,t,e,r,i){var u=n[0]-t[0],o=n[1]-t[1],a=(i?r:-r)/Math.sqrt(u*u+o*o),l=a*o,c=-a*u,f=n[0]+l,s=n[1]+c,h=t[0]+l,p=t[1]+c,g=(f+h)/2,v=(s+p)/2,d=h-f,y=p-s,m=d*d+y*y,M=e-r,x=f*p-h*s,b=(0>y?-1:1)*Math.sqrt(Math.max(0,M*M*m-x*x)),_=(x*y-d*b)/m,w=(-x*d-y*b)/m,S=(x*y+d*b)/m,k=(-x*d+y*b)/m,N=_-g,E=w-v,A=S-g,C=k-v;return N*N+E*E>A*A+C*C&&(_=S,w=k),[[_-l,w-c],[_*e/M,w*e/M]]}function Mu(n){function t(t){function o(){c.push("M",u(n(f),a))}for(var l,c=[],f=[],s=-1,h=t.length,p=En(e),g=En(r);++s1?n.join("L"):n+"Z"}function bu(n){return n.join("L")+"Z"}function _u(n){for(var t=0,e=n.length,r=n[0],i=[r[0],",",r[1]];++t1&&i.push("H",r[0]),i.join("")}function wu(n){for(var t=0,e=n.length,r=n[0],i=[r[0],",",r[1]];++t1){a=t[1],u=n[l],l++,r+="C"+(i[0]+o[0])+","+(i[1]+o[1])+","+(u[0]-a[0])+","+(u[1]-a[1])+","+u[0]+","+u[1];for(var c=2;c9&&(i=3*t/Math.sqrt(i),o[a]=i*e,o[a+1]=i*r));for(a=-1;++a<=l;)i=(n[Math.min(l,a+1)][0]-n[Math.max(0,a-1)][0])/(6*(1+o[a]*o[a])),u.push([i||0,o[a]*i||0]);return u}function Fu(n){return n.length<3?xu(n):n[0]+Au(n,ju(n))}function Hu(n){for(var t,e,r,i=-1,u=n.length;++i=t?o(n-t):void(f.c=o)}function o(e){var i=g.active,u=g[i];u&&(u.timer.c=null,u.timer.t=NaN,--g.count,delete g[i],u.event&&u.event.interrupt.call(n,n.__data__,u.index));for(var o in g)if(r>+o){var c=g[o];c.timer.c=null,c.timer.t=NaN,--g.count,delete g[o]}f.c=a,qn(function(){return f.c&&a(e||1)&&(f.c=null,f.t=NaN),1},0,l),g.active=r,v.event&&v.event.start.call(n,n.__data__,t),p=[],v.tween.forEach(function(e,r){(r=r.call(n,n.__data__,t))&&p.push(r)}),h=v.ease,s=v.duration}function a(i){for(var u=i/s,o=h(u),a=p.length;a>0;)p[--a].call(n,o);return u>=1?(v.event&&v.event.end.call(n,n.__data__,t),--g.count?delete g[r]:delete n[e],1):void 0}var l,f,s,h,p,g=n[e]||(n[e]={active:0,count:0}),v=g[r];v||(l=i.time,f=qn(u,0,l),v=g[r]={tween:new c,time:l,timer:f,delay:i.delay,duration:i.duration,ease:i.ease,index:t},i=null,++g.count)}function no(n,t,e){n.attr("transform",function(n){var r=t(n);return"translate("+(isFinite(r)?r:e(n))+",0)"})}function to(n,t,e){n.attr("transform",function(n){var r=t(n);return"translate(0,"+(isFinite(r)?r:e(n))+")"})}function eo(n){return n.toISOString()}function ro(n,t,e){function r(t){return n(t)}function i(n,e){var r=n[1]-n[0],i=r/e,u=ao.bisect(Kl,i);return u==Kl.length?[t.year,Ki(n.map(function(n){return n/31536e6}),e)[2]]:u?t[i/Kl[u-1]1?{floor:function(t){for(;e(t=n.floor(t));)t=io(t-1);return t},ceil:function(t){for(;e(t=n.ceil(t));)t=io(+t+1);return t}}:n))},r.ticks=function(n,t){var e=Yi(r.domain()),u=null==n?i(e,10):"number"==typeof n?i(e,n):!n.range&&[{range:n},t];return u&&(n=u[0],t=u[1]),n.range(e[0],io(+e[1]+1),1>t?1:t)},r.tickFormat=function(){return e},r.copy=function(){return ro(n.copy(),t,e)},Ji(r,n)}function io(n){return new Date(n)}function uo(n){return JSON.parse(n.responseText)}function oo(n){var t=fo.createRange();return t.selectNode(fo.body),t.createContextualFragment(n.responseText)}var ao={version:"3.5.17"},lo=[].slice,co=function(n){return lo.call(n)},fo=this.document;if(fo)try{co(fo.documentElement.childNodes)[0].nodeType}catch(so){co=function(n){for(var t=n.length,e=new Array(t);t--;)e[t]=n[t];return e}}if(Date.now||(Date.now=function(){return+new Date}),fo)try{fo.createElement("DIV").style.setProperty("opacity",0,"")}catch(ho){var po=this.Element.prototype,go=po.setAttribute,vo=po.setAttributeNS,yo=this.CSSStyleDeclaration.prototype,mo=yo.setProperty;po.setAttribute=function(n,t){go.call(this,n,t+"")},po.setAttributeNS=function(n,t,e){vo.call(this,n,t,e+"")},yo.setProperty=function(n,t,e){mo.call(this,n,t+"",e)}}ao.ascending=e,ao.descending=function(n,t){return n>t?-1:t>n?1:t>=n?0:NaN},ao.min=function(n,t){var e,r,i=-1,u=n.length;if(1===arguments.length){for(;++i=r){e=r;break}for(;++ir&&(e=r)}else{for(;++i=r){e=r;break}for(;++ir&&(e=r)}return e},ao.max=function(n,t){var e,r,i=-1,u=n.length;if(1===arguments.length){for(;++i=r){e=r;break}for(;++ie&&(e=r)}else{for(;++i=r){e=r;break}for(;++ie&&(e=r)}return e},ao.extent=function(n,t){var e,r,i,u=-1,o=n.length;if(1===arguments.length){for(;++u=r){e=i=r;break}for(;++ur&&(e=r),r>i&&(i=r))}else{for(;++u=r){e=i=r;break}for(;++ur&&(e=r),r>i&&(i=r))}return[e,i]},ao.sum=function(n,t){var e,r=0,u=n.length,o=-1;if(1===arguments.length)for(;++o1?l/(f-1):void 0},ao.deviation=function(){var n=ao.variance.apply(this,arguments);return n?Math.sqrt(n):n};var Mo=u(e);ao.bisectLeft=Mo.left,ao.bisect=ao.bisectRight=Mo.right,ao.bisector=function(n){return u(1===n.length?function(t,r){return e(n(t),r)}:n)},ao.shuffle=function(n,t,e){(u=arguments.length)<3&&(e=n.length,2>u&&(t=0));for(var r,i,u=e-t;u;)i=Math.random()*u--|0,r=n[u+t],n[u+t]=n[i+t],n[i+t]=r;return n},ao.permute=function(n,t){for(var e=t.length,r=new Array(e);e--;)r[e]=n[t[e]];return r},ao.pairs=function(n){for(var t,e=0,r=n.length-1,i=n[0],u=new Array(0>r?0:r);r>e;)u[e]=[t=i,i=n[++e]];return u},ao.transpose=function(n){if(!(i=n.length))return[];for(var t=-1,e=ao.min(n,o),r=new Array(e);++t=0;)for(r=n[i],t=r.length;--t>=0;)e[--o]=r[t];return e};var xo=Math.abs;ao.range=function(n,t,e){if(arguments.length<3&&(e=1,arguments.length<2&&(t=n,n=0)),(t-n)/e===1/0)throw new Error("infinite range");var r,i=[],u=a(xo(e)),o=-1;if(n*=u,t*=u,e*=u,0>e)for(;(r=n+e*++o)>t;)i.push(r/u);else for(;(r=n+e*++o)=u.length)return r?r.call(i,o):e?o.sort(e):o;for(var l,f,s,h,p=-1,g=o.length,v=u[a++],d=new c;++p=u.length)return n;var r=[],i=o[e++];return n.forEach(function(n,i){r.push({key:n,values:t(i,e)})}),i?r.sort(function(n,t){return i(n.key,t.key)}):r}var e,r,i={},u=[],o=[];return i.map=function(t,e){return n(e,t,0)},i.entries=function(e){return t(n(ao.map,e,0),0)},i.key=function(n){return u.push(n),i},i.sortKeys=function(n){return o[u.length-1]=n,i},i.sortValues=function(n){return e=n,i},i.rollup=function(n){return r=n,i},i},ao.set=function(n){var t=new y;if(n)for(var e=0,r=n.length;r>e;++e)t.add(n[e]);return t},l(y,{has:h,add:function(n){return this._[f(n+="")]=!0,n},remove:p,values:g,size:v,empty:d,forEach:function(n){for(var t in this._)n.call(this,s(t))}}),ao.behavior={},ao.rebind=function(n,t){for(var e,r=1,i=arguments.length;++r=0&&(r=n.slice(e+1),n=n.slice(0,e)),n)return arguments.length<2?this[n].on(r):this[n].on(r,t);if(2===arguments.length){if(null==t)for(n in this)this.hasOwnProperty(n)&&this[n].on(r,null);return this}},ao.event=null,ao.requote=function(n){return n.replace(So,"\\$&")};var So=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g,ko={}.__proto__?function(n,t){n.__proto__=t}:function(n,t){for(var e in t)n[e]=t[e]},No=function(n,t){return t.querySelector(n)},Eo=function(n,t){return t.querySelectorAll(n)},Ao=function(n,t){var e=n.matches||n[x(n,"matchesSelector")];return(Ao=function(n,t){return e.call(n,t)})(n,t)};"function"==typeof Sizzle&&(No=function(n,t){return Sizzle(n,t)[0]||null},Eo=Sizzle,Ao=Sizzle.matchesSelector),ao.selection=function(){return ao.select(fo.documentElement)};var Co=ao.selection.prototype=[];Co.select=function(n){var t,e,r,i,u=[];n=A(n);for(var o=-1,a=this.length;++o=0&&"xmlns"!==(e=n.slice(0,t))&&(n=n.slice(t+1)),Lo.hasOwnProperty(e)?{space:Lo[e],local:n}:n}},Co.attr=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node();return n=ao.ns.qualify(n),n.local?e.getAttributeNS(n.space,n.local):e.getAttribute(n)}for(t in n)this.each(z(t,n[t]));return this}return this.each(z(n,t))},Co.classed=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node(),r=(n=T(n)).length,i=-1;if(t=e.classList){for(;++ii){if("string"!=typeof n){2>i&&(e="");for(r in n)this.each(P(r,n[r],e));return this}if(2>i){var u=this.node();return t(u).getComputedStyle(u,null).getPropertyValue(n)}r=""}return this.each(P(n,e,r))},Co.property=function(n,t){if(arguments.length<2){if("string"==typeof n)return this.node()[n];for(t in n)this.each(U(t,n[t]));return this}return this.each(U(n,t))},Co.text=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.textContent=null==t?"":t}:null==n?function(){this.textContent=""}:function(){this.textContent=n}):this.node().textContent},Co.html=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.innerHTML=null==t?"":t}:null==n?function(){this.innerHTML=""}:function(){this.innerHTML=n}):this.node().innerHTML},Co.append=function(n){return n=j(n),this.select(function(){return this.appendChild(n.apply(this,arguments))})},Co.insert=function(n,t){return n=j(n),t=A(t),this.select(function(){return this.insertBefore(n.apply(this,arguments),t.apply(this,arguments)||null)})},Co.remove=function(){return this.each(F)},Co.data=function(n,t){function e(n,e){var r,i,u,o=n.length,s=e.length,h=Math.min(o,s),p=new Array(s),g=new Array(s),v=new Array(o);if(t){var d,y=new c,m=new Array(o);for(r=-1;++rr;++r)g[r]=H(e[r]);for(;o>r;++r)v[r]=n[r]}g.update=p,g.parentNode=p.parentNode=v.parentNode=n.parentNode,a.push(g),l.push(p),f.push(v)}var r,i,u=-1,o=this.length;if(!arguments.length){for(n=new Array(o=(r=this[0]).length);++uu;u++){i.push(t=[]),t.parentNode=(e=this[u]).parentNode;for(var a=0,l=e.length;l>a;a++)(r=e[a])&&n.call(r,r.__data__,a,u)&&t.push(r)}return E(i)},Co.order=function(){for(var n=-1,t=this.length;++n=0;)(e=r[i])&&(u&&u!==e.nextSibling&&u.parentNode.insertBefore(e,u),u=e);return this},Co.sort=function(n){n=I.apply(this,arguments);for(var t=-1,e=this.length;++tn;n++)for(var e=this[n],r=0,i=e.length;i>r;r++){var u=e[r];if(u)return u}return null},Co.size=function(){var n=0;return Y(this,function(){++n}),n};var qo=[];ao.selection.enter=Z,ao.selection.enter.prototype=qo,qo.append=Co.append,qo.empty=Co.empty,qo.node=Co.node,qo.call=Co.call,qo.size=Co.size,qo.select=function(n){for(var t,e,r,i,u,o=[],a=-1,l=this.length;++ar){if("string"!=typeof n){2>r&&(t=!1);for(e in n)this.each(X(e,n[e],t));return this}if(2>r)return(r=this.node()["__on"+n])&&r._;e=!1}return this.each(X(n,t,e))};var To=ao.map({mouseenter:"mouseover",mouseleave:"mouseout"});fo&&To.forEach(function(n){"on"+n in fo&&To.remove(n)});var Ro,Do=0;ao.mouse=function(n){return J(n,k())};var Po=this.navigator&&/WebKit/.test(this.navigator.userAgent)?-1:0;ao.touch=function(n,t,e){if(arguments.length<3&&(e=t,t=k().changedTouches),t)for(var r,i=0,u=t.length;u>i;++i)if((r=t[i]).identifier===e)return J(n,r)},ao.behavior.drag=function(){function n(){this.on("mousedown.drag",u).on("touchstart.drag",o)}function e(n,t,e,u,o){return function(){function a(){var n,e,r=t(h,v);r&&(n=r[0]-M[0],e=r[1]-M[1],g|=n|e,M=r,p({type:"drag",x:r[0]+c[0],y:r[1]+c[1],dx:n,dy:e}))}function l(){t(h,v)&&(y.on(u+d,null).on(o+d,null),m(g),p({type:"dragend"}))}var c,f=this,s=ao.event.target.correspondingElement||ao.event.target,h=f.parentNode,p=r.of(f,arguments),g=0,v=n(),d=".drag"+(null==v?"":"-"+v),y=ao.select(e(s)).on(u+d,a).on(o+d,l),m=W(s),M=t(h,v);i?(c=i.apply(f,arguments),c=[c.x-M[0],c.y-M[1]]):c=[0,0],p({type:"dragstart"})}}var r=N(n,"drag","dragstart","dragend"),i=null,u=e(b,ao.mouse,t,"mousemove","mouseup"),o=e(G,ao.touch,m,"touchmove","touchend");return n.origin=function(t){return arguments.length?(i=t,n):i},ao.rebind(n,r,"on")},ao.touches=function(n,t){return arguments.length<2&&(t=k().touches),t?co(t).map(function(t){var e=J(n,t);return e.identifier=t.identifier,e}):[]};var Uo=1e-6,jo=Uo*Uo,Fo=Math.PI,Ho=2*Fo,Oo=Ho-Uo,Io=Fo/2,Yo=Fo/180,Zo=180/Fo,Vo=Math.SQRT2,Xo=2,$o=4;ao.interpolateZoom=function(n,t){var e,r,i=n[0],u=n[1],o=n[2],a=t[0],l=t[1],c=t[2],f=a-i,s=l-u,h=f*f+s*s;if(jo>h)r=Math.log(c/o)/Vo,e=function(n){return[i+n*f,u+n*s,o*Math.exp(Vo*n*r)]};else{var p=Math.sqrt(h),g=(c*c-o*o+$o*h)/(2*o*Xo*p),v=(c*c-o*o-$o*h)/(2*c*Xo*p),d=Math.log(Math.sqrt(g*g+1)-g),y=Math.log(Math.sqrt(v*v+1)-v);r=(y-d)/Vo,e=function(n){var t=n*r,e=rn(d),a=o/(Xo*p)*(e*un(Vo*t+d)-en(d));return[i+a*f,u+a*s,o*e/rn(Vo*t+d)]}}return e.duration=1e3*r,e},ao.behavior.zoom=function(){function n(n){n.on(L,s).on(Wo+".zoom",p).on("dblclick.zoom",g).on(R,h)}function e(n){return[(n[0]-k.x)/k.k,(n[1]-k.y)/k.k]}function r(n){return[n[0]*k.k+k.x,n[1]*k.k+k.y]}function i(n){k.k=Math.max(A[0],Math.min(A[1],n))}function u(n,t){t=r(t),k.x+=n[0]-t[0],k.y+=n[1]-t[1]}function o(t,e,r,o){t.__chart__={x:k.x,y:k.y,k:k.k},i(Math.pow(2,o)),u(d=e,r),t=ao.select(t),C>0&&(t=t.transition().duration(C)),t.call(n.event)}function a(){b&&b.domain(x.range().map(function(n){return(n-k.x)/k.k}).map(x.invert)),w&&w.domain(_.range().map(function(n){return(n-k.y)/k.k}).map(_.invert))}function l(n){z++||n({type:"zoomstart"})}function c(n){a(),n({type:"zoom",scale:k.k,translate:[k.x,k.y]})}function f(n){--z||(n({type:"zoomend"}),d=null)}function s(){function n(){a=1,u(ao.mouse(i),h),c(o)}function r(){s.on(q,null).on(T,null),p(a),f(o)}var i=this,o=D.of(i,arguments),a=0,s=ao.select(t(i)).on(q,n).on(T,r),h=e(ao.mouse(i)),p=W(i);Il.call(i),l(o)}function h(){function n(){var n=ao.touches(g);return p=k.k,n.forEach(function(n){n.identifier in d&&(d[n.identifier]=e(n))}),n}function t(){var t=ao.event.target;ao.select(t).on(x,r).on(b,a),_.push(t);for(var e=ao.event.changedTouches,i=0,u=e.length;u>i;++i)d[e[i].identifier]=null;var l=n(),c=Date.now();if(1===l.length){if(500>c-M){var f=l[0];o(g,f,d[f.identifier],Math.floor(Math.log(k.k)/Math.LN2)+1),S()}M=c}else if(l.length>1){var f=l[0],s=l[1],h=f[0]-s[0],p=f[1]-s[1];y=h*h+p*p}}function r(){var n,t,e,r,o=ao.touches(g);Il.call(g);for(var a=0,l=o.length;l>a;++a,r=null)if(e=o[a],r=d[e.identifier]){if(t)break;n=e,t=r}if(r){var f=(f=e[0]-n[0])*f+(f=e[1]-n[1])*f,s=y&&Math.sqrt(f/y);n=[(n[0]+e[0])/2,(n[1]+e[1])/2],t=[(t[0]+r[0])/2,(t[1]+r[1])/2],i(s*p)}M=null,u(n,t),c(v)}function a(){if(ao.event.touches.length){for(var t=ao.event.changedTouches,e=0,r=t.length;r>e;++e)delete d[t[e].identifier];for(var i in d)return void n()}ao.selectAll(_).on(m,null),w.on(L,s).on(R,h),N(),f(v)}var p,g=this,v=D.of(g,arguments),d={},y=0,m=".zoom-"+ao.event.changedTouches[0].identifier,x="touchmove"+m,b="touchend"+m,_=[],w=ao.select(g),N=W(g);t(),l(v),w.on(L,null).on(R,t)}function p(){var n=D.of(this,arguments);m?clearTimeout(m):(Il.call(this),v=e(d=y||ao.mouse(this)),l(n)),m=setTimeout(function(){m=null,f(n)},50),S(),i(Math.pow(2,.002*Bo())*k.k),u(d,v),c(n)}function g(){var n=ao.mouse(this),t=Math.log(k.k)/Math.LN2;o(this,n,e(n),ao.event.shiftKey?Math.ceil(t)-1:Math.floor(t)+1)}var v,d,y,m,M,x,b,_,w,k={x:0,y:0,k:1},E=[960,500],A=Jo,C=250,z=0,L="mousedown.zoom",q="mousemove.zoom",T="mouseup.zoom",R="touchstart.zoom",D=N(n,"zoomstart","zoom","zoomend");return Wo||(Wo="onwheel"in fo?(Bo=function(){return-ao.event.deltaY*(ao.event.deltaMode?120:1)},"wheel"):"onmousewheel"in fo?(Bo=function(){return ao.event.wheelDelta},"mousewheel"):(Bo=function(){return-ao.event.detail},"MozMousePixelScroll")),n.event=function(n){n.each(function(){var n=D.of(this,arguments),t=k;Hl?ao.select(this).transition().each("start.zoom",function(){k=this.__chart__||{x:0,y:0,k:1},l(n)}).tween("zoom:zoom",function(){var e=E[0],r=E[1],i=d?d[0]:e/2,u=d?d[1]:r/2,o=ao.interpolateZoom([(i-k.x)/k.k,(u-k.y)/k.k,e/k.k],[(i-t.x)/t.k,(u-t.y)/t.k,e/t.k]);return function(t){var r=o(t),a=e/r[2];this.__chart__=k={x:i-r[0]*a,y:u-r[1]*a,k:a},c(n)}}).each("interrupt.zoom",function(){f(n)}).each("end.zoom",function(){f(n)}):(this.__chart__=k,l(n),c(n),f(n))})},n.translate=function(t){return arguments.length?(k={x:+t[0],y:+t[1],k:k.k},a(),n):[k.x,k.y]},n.scale=function(t){return arguments.length?(k={x:k.x,y:k.y,k:null},i(+t),a(),n):k.k},n.scaleExtent=function(t){return arguments.length?(A=null==t?Jo:[+t[0],+t[1]],n):A},n.center=function(t){return arguments.length?(y=t&&[+t[0],+t[1]],n):y},n.size=function(t){return arguments.length?(E=t&&[+t[0],+t[1]],n):E},n.duration=function(t){return arguments.length?(C=+t,n):C},n.x=function(t){return arguments.length?(b=t,x=t.copy(),k={x:0,y:0,k:1},n):b},n.y=function(t){return arguments.length?(w=t,_=t.copy(),k={x:0,y:0,k:1},n):w},ao.rebind(n,D,"on")};var Bo,Wo,Jo=[0,1/0];ao.color=an,an.prototype.toString=function(){return this.rgb()+""},ao.hsl=ln;var Go=ln.prototype=new an;Go.brighter=function(n){return n=Math.pow(.7,arguments.length?n:1),new ln(this.h,this.s,this.l/n)},Go.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),new ln(this.h,this.s,n*this.l)},Go.rgb=function(){return cn(this.h,this.s,this.l)},ao.hcl=fn;var Ko=fn.prototype=new an;Ko.brighter=function(n){return new fn(this.h,this.c,Math.min(100,this.l+Qo*(arguments.length?n:1)))},Ko.darker=function(n){return new fn(this.h,this.c,Math.max(0,this.l-Qo*(arguments.length?n:1)))},Ko.rgb=function(){return sn(this.h,this.c,this.l).rgb()},ao.lab=hn;var Qo=18,na=.95047,ta=1,ea=1.08883,ra=hn.prototype=new an;ra.brighter=function(n){return new hn(Math.min(100,this.l+Qo*(arguments.length?n:1)),this.a,this.b)},ra.darker=function(n){return new hn(Math.max(0,this.l-Qo*(arguments.length?n:1)),this.a,this.b)},ra.rgb=function(){return pn(this.l,this.a,this.b)},ao.rgb=mn;var ia=mn.prototype=new an;ia.brighter=function(n){n=Math.pow(.7,arguments.length?n:1);var t=this.r,e=this.g,r=this.b,i=30;return t||e||r?(t&&i>t&&(t=i),e&&i>e&&(e=i),r&&i>r&&(r=i),new mn(Math.min(255,t/n),Math.min(255,e/n),Math.min(255,r/n))):new mn(i,i,i)},ia.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),new mn(n*this.r,n*this.g,n*this.b)},ia.hsl=function(){return wn(this.r,this.g,this.b)},ia.toString=function(){return"#"+bn(this.r)+bn(this.g)+bn(this.b)};var ua=ao.map({aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074});ua.forEach(function(n,t){ua.set(n,Mn(t))}),ao.functor=En,ao.xhr=An(m),ao.dsv=function(n,t){function e(n,e,u){arguments.length<3&&(u=e,e=null);var o=Cn(n,t,null==e?r:i(e),u);return o.row=function(n){return arguments.length?o.response(null==(e=n)?r:i(n)):e},o}function r(n){return e.parse(n.responseText)}function i(n){return function(t){return e.parse(t.responseText,n)}}function u(t){return t.map(o).join(n)}function o(n){return a.test(n)?'"'+n.replace(/\"/g,'""')+'"':n}var a=new RegExp('["'+n+"\n]"),l=n.charCodeAt(0);return e.parse=function(n,t){var r;return e.parseRows(n,function(n,e){if(r)return r(n,e-1);var i=new Function("d","return {"+n.map(function(n,t){return JSON.stringify(n)+": d["+t+"]"}).join(",")+"}");r=t?function(n,e){return t(i(n),e)}:i})},e.parseRows=function(n,t){function e(){if(f>=c)return o;if(i)return i=!1,u;var t=f;if(34===n.charCodeAt(t)){for(var e=t;e++f;){var r=n.charCodeAt(f++),a=1;if(10===r)i=!0;else if(13===r)i=!0,10===n.charCodeAt(f)&&(++f,++a);else if(r!==l)continue;return n.slice(t,f-a)}return n.slice(t)}for(var r,i,u={},o={},a=[],c=n.length,f=0,s=0;(r=e())!==o;){for(var h=[];r!==u&&r!==o;)h.push(r),r=e();t&&null==(h=t(h,s++))||a.push(h)}return a},e.format=function(t){if(Array.isArray(t[0]))return e.formatRows(t);var r=new y,i=[];return t.forEach(function(n){for(var t in n)r.has(t)||i.push(r.add(t))}),[i.map(o).join(n)].concat(t.map(function(t){return i.map(function(n){return o(t[n])}).join(n)})).join("\n")},e.formatRows=function(n){return n.map(u).join("\n")},e},ao.csv=ao.dsv(",","text/csv"),ao.tsv=ao.dsv(" ","text/tab-separated-values");var oa,aa,la,ca,fa=this[x(this,"requestAnimationFrame")]||function(n){setTimeout(n,17)};ao.timer=function(){qn.apply(this,arguments)},ao.timer.flush=function(){Rn(),Dn()},ao.round=function(n,t){return t?Math.round(n*(t=Math.pow(10,t)))/t:Math.round(n)};var sa=["y","z","a","f","p","n","\xb5","m","","k","M","G","T","P","E","Z","Y"].map(Un);ao.formatPrefix=function(n,t){var e=0;return(n=+n)&&(0>n&&(n*=-1),t&&(n=ao.round(n,Pn(n,t))),e=1+Math.floor(1e-12+Math.log(n)/Math.LN10),e=Math.max(-24,Math.min(24,3*Math.floor((e-1)/3)))),sa[8+e/3]};var ha=/(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i,pa=ao.map({b:function(n){return n.toString(2)},c:function(n){return String.fromCharCode(n)},o:function(n){return n.toString(8)},x:function(n){return n.toString(16)},X:function(n){return n.toString(16).toUpperCase()},g:function(n,t){return n.toPrecision(t)},e:function(n,t){return n.toExponential(t)},f:function(n,t){return n.toFixed(t)},r:function(n,t){return(n=ao.round(n,Pn(n,t))).toFixed(Math.max(0,Math.min(20,Pn(n*(1+1e-15),t))))}}),ga=ao.time={},va=Date;Hn.prototype={getDate:function(){return this._.getUTCDate()},getDay:function(){return this._.getUTCDay()},getFullYear:function(){return this._.getUTCFullYear()},getHours:function(){return this._.getUTCHours()},getMilliseconds:function(){return this._.getUTCMilliseconds()},getMinutes:function(){return this._.getUTCMinutes()},getMonth:function(){return this._.getUTCMonth()},getSeconds:function(){return this._.getUTCSeconds()},getTime:function(){return this._.getTime()},getTimezoneOffset:function(){return 0},valueOf:function(){return this._.valueOf()},setDate:function(){da.setUTCDate.apply(this._,arguments)},setDay:function(){da.setUTCDay.apply(this._,arguments)},setFullYear:function(){da.setUTCFullYear.apply(this._,arguments)},setHours:function(){da.setUTCHours.apply(this._,arguments)},setMilliseconds:function(){da.setUTCMilliseconds.apply(this._,arguments)},setMinutes:function(){da.setUTCMinutes.apply(this._,arguments)},setMonth:function(){da.setUTCMonth.apply(this._,arguments)},setSeconds:function(){da.setUTCSeconds.apply(this._,arguments)},setTime:function(){da.setTime.apply(this._,arguments)}};var da=Date.prototype;ga.year=On(function(n){return n=ga.day(n),n.setMonth(0,1),n},function(n,t){n.setFullYear(n.getFullYear()+t)},function(n){return n.getFullYear()}),ga.years=ga.year.range,ga.years.utc=ga.year.utc.range,ga.day=On(function(n){var t=new va(2e3,0);return t.setFullYear(n.getFullYear(),n.getMonth(),n.getDate()),t},function(n,t){n.setDate(n.getDate()+t)},function(n){return n.getDate()-1}),ga.days=ga.day.range,ga.days.utc=ga.day.utc.range,ga.dayOfYear=function(n){var t=ga.year(n);return Math.floor((n-t-6e4*(n.getTimezoneOffset()-t.getTimezoneOffset()))/864e5)},["sunday","monday","tuesday","wednesday","thursday","friday","saturday"].forEach(function(n,t){t=7-t;var e=ga[n]=On(function(n){return(n=ga.day(n)).setDate(n.getDate()-(n.getDay()+t)%7),n},function(n,t){n.setDate(n.getDate()+7*Math.floor(t))},function(n){var e=ga.year(n).getDay();return Math.floor((ga.dayOfYear(n)+(e+t)%7)/7)-(e!==t)});ga[n+"s"]=e.range,ga[n+"s"].utc=e.utc.range,ga[n+"OfYear"]=function(n){var e=ga.year(n).getDay();return Math.floor((ga.dayOfYear(n)+(e+t)%7)/7)}}),ga.week=ga.sunday,ga.weeks=ga.sunday.range,ga.weeks.utc=ga.sunday.utc.range,ga.weekOfYear=ga.sundayOfYear;var ya={"-":"",_:" ",0:"0"},ma=/^\s*\d+/,Ma=/^%/;ao.locale=function(n){return{numberFormat:jn(n),timeFormat:Yn(n)}};var xa=ao.locale({decimal:".",thousands:",",grouping:[3],currency:["$",""],dateTime:"%a %b %e %X %Y",date:"%m/%d/%Y",time:"%H:%M:%S",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],
+shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});ao.format=xa.numberFormat,ao.geo={},ft.prototype={s:0,t:0,add:function(n){st(n,this.t,ba),st(ba.s,this.s,this),this.s?this.t+=ba.t:this.s=ba.t},reset:function(){this.s=this.t=0},valueOf:function(){return this.s}};var ba=new ft;ao.geo.stream=function(n,t){n&&_a.hasOwnProperty(n.type)?_a[n.type](n,t):ht(n,t)};var _a={Feature:function(n,t){ht(n.geometry,t)},FeatureCollection:function(n,t){for(var e=n.features,r=-1,i=e.length;++rn?4*Fo+n:n,Na.lineStart=Na.lineEnd=Na.point=b}};ao.geo.bounds=function(){function n(n,t){M.push(x=[f=n,h=n]),s>t&&(s=t),t>p&&(p=t)}function t(t,e){var r=dt([t*Yo,e*Yo]);if(y){var i=mt(y,r),u=[i[1],-i[0],0],o=mt(u,i);bt(o),o=_t(o);var l=t-g,c=l>0?1:-1,v=o[0]*Zo*c,d=xo(l)>180;if(d^(v>c*g&&c*t>v)){var m=o[1]*Zo;m>p&&(p=m)}else if(v=(v+360)%360-180,d^(v>c*g&&c*t>v)){var m=-o[1]*Zo;s>m&&(s=m)}else s>e&&(s=e),e>p&&(p=e);d?g>t?a(f,t)>a(f,h)&&(h=t):a(t,h)>a(f,h)&&(f=t):h>=f?(f>t&&(f=t),t>h&&(h=t)):t>g?a(f,t)>a(f,h)&&(h=t):a(t,h)>a(f,h)&&(f=t)}else n(t,e);y=r,g=t}function e(){b.point=t}function r(){x[0]=f,x[1]=h,b.point=n,y=null}function i(n,e){if(y){var r=n-g;m+=xo(r)>180?r+(r>0?360:-360):r}else v=n,d=e;Na.point(n,e),t(n,e)}function u(){Na.lineStart()}function o(){i(v,d),Na.lineEnd(),xo(m)>Uo&&(f=-(h=180)),x[0]=f,x[1]=h,y=null}function a(n,t){return(t-=n)<0?t+360:t}function l(n,t){return n[0]-t[0]}function c(n,t){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:nka?(f=-(h=180),s=-(p=90)):m>Uo?p=90:-Uo>m&&(s=-90),x[0]=f,x[1]=h}};return function(n){p=h=-(f=s=1/0),M=[],ao.geo.stream(n,b);var t=M.length;if(t){M.sort(l);for(var e,r=1,i=M[0],u=[i];t>r;++r)e=M[r],c(e[0],i)||c(e[1],i)?(a(i[0],e[1])>a(i[0],i[1])&&(i[1]=e[1]),a(e[0],i[1])>a(i[0],i[1])&&(i[0]=e[0])):u.push(i=e);for(var o,e,g=-(1/0),t=u.length-1,r=0,i=u[t];t>=r;i=e,++r)e=u[r],(o=a(i[1],e[0]))>g&&(g=o,f=e[0],h=i[1])}return M=x=null,f===1/0||s===1/0?[[NaN,NaN],[NaN,NaN]]:[[f,s],[h,p]]}}(),ao.geo.centroid=function(n){Ea=Aa=Ca=za=La=qa=Ta=Ra=Da=Pa=Ua=0,ao.geo.stream(n,ja);var t=Da,e=Pa,r=Ua,i=t*t+e*e+r*r;return jo>i&&(t=qa,e=Ta,r=Ra,Uo>Aa&&(t=Ca,e=za,r=La),i=t*t+e*e+r*r,jo>i)?[NaN,NaN]:[Math.atan2(e,t)*Zo,tn(r/Math.sqrt(i))*Zo]};var Ea,Aa,Ca,za,La,qa,Ta,Ra,Da,Pa,Ua,ja={sphere:b,point:St,lineStart:Nt,lineEnd:Et,polygonStart:function(){ja.lineStart=At},polygonEnd:function(){ja.lineStart=Nt}},Fa=Rt(zt,jt,Ht,[-Fo,-Fo/2]),Ha=1e9;ao.geo.clipExtent=function(){var n,t,e,r,i,u,o={stream:function(n){return i&&(i.valid=!1),i=u(n),i.valid=!0,i},extent:function(a){return arguments.length?(u=Zt(n=+a[0][0],t=+a[0][1],e=+a[1][0],r=+a[1][1]),i&&(i.valid=!1,i=null),o):[[n,t],[e,r]]}};return o.extent([[0,0],[960,500]])},(ao.geo.conicEqualArea=function(){return Vt(Xt)}).raw=Xt,ao.geo.albers=function(){return ao.geo.conicEqualArea().rotate([96,0]).center([-.6,38.7]).parallels([29.5,45.5]).scale(1070)},ao.geo.albersUsa=function(){function n(n){var u=n[0],o=n[1];return t=null,e(u,o),t||(r(u,o),t)||i(u,o),t}var t,e,r,i,u=ao.geo.albers(),o=ao.geo.conicEqualArea().rotate([154,0]).center([-2,58.5]).parallels([55,65]),a=ao.geo.conicEqualArea().rotate([157,0]).center([-3,19.9]).parallels([8,18]),l={point:function(n,e){t=[n,e]}};return n.invert=function(n){var t=u.scale(),e=u.translate(),r=(n[0]-e[0])/t,i=(n[1]-e[1])/t;return(i>=.12&&.234>i&&r>=-.425&&-.214>r?o:i>=.166&&.234>i&&r>=-.214&&-.115>r?a:u).invert(n)},n.stream=function(n){var t=u.stream(n),e=o.stream(n),r=a.stream(n);return{point:function(n,i){t.point(n,i),e.point(n,i),r.point(n,i)},sphere:function(){t.sphere(),e.sphere(),r.sphere()},lineStart:function(){t.lineStart(),e.lineStart(),r.lineStart()},lineEnd:function(){t.lineEnd(),e.lineEnd(),r.lineEnd()},polygonStart:function(){t.polygonStart(),e.polygonStart(),r.polygonStart()},polygonEnd:function(){t.polygonEnd(),e.polygonEnd(),r.polygonEnd()}}},n.precision=function(t){return arguments.length?(u.precision(t),o.precision(t),a.precision(t),n):u.precision()},n.scale=function(t){return arguments.length?(u.scale(t),o.scale(.35*t),a.scale(t),n.translate(u.translate())):u.scale()},n.translate=function(t){if(!arguments.length)return u.translate();var c=u.scale(),f=+t[0],s=+t[1];return e=u.translate(t).clipExtent([[f-.455*c,s-.238*c],[f+.455*c,s+.238*c]]).stream(l).point,r=o.translate([f-.307*c,s+.201*c]).clipExtent([[f-.425*c+Uo,s+.12*c+Uo],[f-.214*c-Uo,s+.234*c-Uo]]).stream(l).point,i=a.translate([f-.205*c,s+.212*c]).clipExtent([[f-.214*c+Uo,s+.166*c+Uo],[f-.115*c-Uo,s+.234*c-Uo]]).stream(l).point,n},n.scale(1070)};var Oa,Ia,Ya,Za,Va,Xa,$a={point:b,lineStart:b,lineEnd:b,polygonStart:function(){Ia=0,$a.lineStart=$t},polygonEnd:function(){$a.lineStart=$a.lineEnd=$a.point=b,Oa+=xo(Ia/2)}},Ba={point:Bt,lineStart:b,lineEnd:b,polygonStart:b,polygonEnd:b},Wa={point:Gt,lineStart:Kt,lineEnd:Qt,polygonStart:function(){Wa.lineStart=ne},polygonEnd:function(){Wa.point=Gt,Wa.lineStart=Kt,Wa.lineEnd=Qt}};ao.geo.path=function(){function n(n){return n&&("function"==typeof a&&u.pointRadius(+a.apply(this,arguments)),o&&o.valid||(o=i(u)),ao.geo.stream(n,o)),u.result()}function t(){return o=null,n}var e,r,i,u,o,a=4.5;return n.area=function(n){return Oa=0,ao.geo.stream(n,i($a)),Oa},n.centroid=function(n){return Ca=za=La=qa=Ta=Ra=Da=Pa=Ua=0,ao.geo.stream(n,i(Wa)),Ua?[Da/Ua,Pa/Ua]:Ra?[qa/Ra,Ta/Ra]:La?[Ca/La,za/La]:[NaN,NaN]},n.bounds=function(n){return Va=Xa=-(Ya=Za=1/0),ao.geo.stream(n,i(Ba)),[[Ya,Za],[Va,Xa]]},n.projection=function(n){return arguments.length?(i=(e=n)?n.stream||re(n):m,t()):e},n.context=function(n){return arguments.length?(u=null==(r=n)?new Wt:new te(n),"function"!=typeof a&&u.pointRadius(a),t()):r},n.pointRadius=function(t){return arguments.length?(a="function"==typeof t?t:(u.pointRadius(+t),+t),n):a},n.projection(ao.geo.albersUsa()).context(null)},ao.geo.transform=function(n){return{stream:function(t){var e=new ie(t);for(var r in n)e[r]=n[r];return e}}},ie.prototype={point:function(n,t){this.stream.point(n,t)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}},ao.geo.projection=oe,ao.geo.projectionMutator=ae,(ao.geo.equirectangular=function(){return oe(ce)}).raw=ce.invert=ce,ao.geo.rotation=function(n){function t(t){return t=n(t[0]*Yo,t[1]*Yo),t[0]*=Zo,t[1]*=Zo,t}return n=se(n[0]%360*Yo,n[1]*Yo,n.length>2?n[2]*Yo:0),t.invert=function(t){return t=n.invert(t[0]*Yo,t[1]*Yo),t[0]*=Zo,t[1]*=Zo,t},t},fe.invert=ce,ao.geo.circle=function(){function n(){var n="function"==typeof r?r.apply(this,arguments):r,t=se(-n[0]*Yo,-n[1]*Yo,0).invert,i=[];return e(null,null,1,{point:function(n,e){i.push(n=t(n,e)),n[0]*=Zo,n[1]*=Zo}}),{type:"Polygon",coordinates:[i]}}var t,e,r=[0,0],i=6;return n.origin=function(t){return arguments.length?(r=t,n):r},n.angle=function(r){return arguments.length?(e=ve((t=+r)*Yo,i*Yo),n):t},n.precision=function(r){return arguments.length?(e=ve(t*Yo,(i=+r)*Yo),n):i},n.angle(90)},ao.geo.distance=function(n,t){var e,r=(t[0]-n[0])*Yo,i=n[1]*Yo,u=t[1]*Yo,o=Math.sin(r),a=Math.cos(r),l=Math.sin(i),c=Math.cos(i),f=Math.sin(u),s=Math.cos(u);return Math.atan2(Math.sqrt((e=s*o)*e+(e=c*f-l*s*a)*e),l*f+c*s*a)},ao.geo.graticule=function(){function n(){return{type:"MultiLineString",coordinates:t()}}function t(){return ao.range(Math.ceil(u/d)*d,i,d).map(h).concat(ao.range(Math.ceil(c/y)*y,l,y).map(p)).concat(ao.range(Math.ceil(r/g)*g,e,g).filter(function(n){return xo(n%d)>Uo}).map(f)).concat(ao.range(Math.ceil(a/v)*v,o,v).filter(function(n){return xo(n%y)>Uo}).map(s))}var e,r,i,u,o,a,l,c,f,s,h,p,g=10,v=g,d=90,y=360,m=2.5;return n.lines=function(){return t().map(function(n){return{type:"LineString",coordinates:n}})},n.outline=function(){return{type:"Polygon",coordinates:[h(u).concat(p(l).slice(1),h(i).reverse().slice(1),p(c).reverse().slice(1))]}},n.extent=function(t){return arguments.length?n.majorExtent(t).minorExtent(t):n.minorExtent()},n.majorExtent=function(t){return arguments.length?(u=+t[0][0],i=+t[1][0],c=+t[0][1],l=+t[1][1],u>i&&(t=u,u=i,i=t),c>l&&(t=c,c=l,l=t),n.precision(m)):[[u,c],[i,l]]},n.minorExtent=function(t){return arguments.length?(r=+t[0][0],e=+t[1][0],a=+t[0][1],o=+t[1][1],r>e&&(t=r,r=e,e=t),a>o&&(t=a,a=o,o=t),n.precision(m)):[[r,a],[e,o]]},n.step=function(t){return arguments.length?n.majorStep(t).minorStep(t):n.minorStep()},n.majorStep=function(t){return arguments.length?(d=+t[0],y=+t[1],n):[d,y]},n.minorStep=function(t){return arguments.length?(g=+t[0],v=+t[1],n):[g,v]},n.precision=function(t){return arguments.length?(m=+t,f=ye(a,o,90),s=me(r,e,m),h=ye(c,l,90),p=me(u,i,m),n):m},n.majorExtent([[-180,-90+Uo],[180,90-Uo]]).minorExtent([[-180,-80-Uo],[180,80+Uo]])},ao.geo.greatArc=function(){function n(){return{type:"LineString",coordinates:[t||r.apply(this,arguments),e||i.apply(this,arguments)]}}var t,e,r=Me,i=xe;return n.distance=function(){return ao.geo.distance(t||r.apply(this,arguments),e||i.apply(this,arguments))},n.source=function(e){return arguments.length?(r=e,t="function"==typeof e?null:e,n):r},n.target=function(t){return arguments.length?(i=t,e="function"==typeof t?null:t,n):i},n.precision=function(){return arguments.length?n:0},n},ao.geo.interpolate=function(n,t){return be(n[0]*Yo,n[1]*Yo,t[0]*Yo,t[1]*Yo)},ao.geo.length=function(n){return Ja=0,ao.geo.stream(n,Ga),Ja};var Ja,Ga={sphere:b,point:b,lineStart:_e,lineEnd:b,polygonStart:b,polygonEnd:b},Ka=we(function(n){return Math.sqrt(2/(1+n))},function(n){return 2*Math.asin(n/2)});(ao.geo.azimuthalEqualArea=function(){return oe(Ka)}).raw=Ka;var Qa=we(function(n){var t=Math.acos(n);return t&&t/Math.sin(t)},m);(ao.geo.azimuthalEquidistant=function(){return oe(Qa)}).raw=Qa,(ao.geo.conicConformal=function(){return Vt(Se)}).raw=Se,(ao.geo.conicEquidistant=function(){return Vt(ke)}).raw=ke;var nl=we(function(n){return 1/n},Math.atan);(ao.geo.gnomonic=function(){return oe(nl)}).raw=nl,Ne.invert=function(n,t){return[n,2*Math.atan(Math.exp(t))-Io]},(ao.geo.mercator=function(){return Ee(Ne)}).raw=Ne;var tl=we(function(){return 1},Math.asin);(ao.geo.orthographic=function(){return oe(tl)}).raw=tl;var el=we(function(n){return 1/(1+n)},function(n){return 2*Math.atan(n)});(ao.geo.stereographic=function(){return oe(el)}).raw=el,Ae.invert=function(n,t){return[-t,2*Math.atan(Math.exp(n))-Io]},(ao.geo.transverseMercator=function(){var n=Ee(Ae),t=n.center,e=n.rotate;return n.center=function(n){return n?t([-n[1],n[0]]):(n=t(),[n[1],-n[0]])},n.rotate=function(n){return n?e([n[0],n[1],n.length>2?n[2]+90:90]):(n=e(),[n[0],n[1],n[2]-90])},e([0,0,90])}).raw=Ae,ao.geom={},ao.geom.hull=function(n){function t(n){if(n.length<3)return[];var t,i=En(e),u=En(r),o=n.length,a=[],l=[];for(t=0;o>t;t++)a.push([+i.call(this,n[t],t),+u.call(this,n[t],t),t]);for(a.sort(qe),t=0;o>t;t++)l.push([a[t][0],-a[t][1]]);var c=Le(a),f=Le(l),s=f[0]===c[0],h=f[f.length-1]===c[c.length-1],p=[];for(t=c.length-1;t>=0;--t)p.push(n[a[c[t]][2]]);for(t=+s;t=r&&c.x<=u&&c.y>=i&&c.y<=o?[[r,o],[u,o],[u,i],[r,i]]:[];f.point=n[a]}),t}function e(n){return n.map(function(n,t){return{x:Math.round(u(n,t)/Uo)*Uo,y:Math.round(o(n,t)/Uo)*Uo,i:t}})}var r=Ce,i=ze,u=r,o=i,a=sl;return n?t(n):(t.links=function(n){return ar(e(n)).edges.filter(function(n){return n.l&&n.r}).map(function(t){return{source:n[t.l.i],target:n[t.r.i]}})},t.triangles=function(n){var t=[];return ar(e(n)).cells.forEach(function(e,r){for(var i,u,o=e.site,a=e.edges.sort(Ve),l=-1,c=a.length,f=a[c-1].edge,s=f.l===o?f.r:f.l;++l=c,h=r>=f,p=h<<1|s;n.leaf=!1,n=n.nodes[p]||(n.nodes[p]=hr()),s?i=c:a=c,h?o=f:l=f,u(n,t,e,r,i,o,a,l)}var f,s,h,p,g,v,d,y,m,M=En(a),x=En(l);if(null!=t)v=t,d=e,y=r,m=i;else if(y=m=-(v=d=1/0),s=[],h=[],g=n.length,o)for(p=0;g>p;++p)f=n[p],f.xy&&(y=f.x),f.y>m&&(m=f.y),s.push(f.x),h.push(f.y);else for(p=0;g>p;++p){var b=+M(f=n[p],p),_=+x(f,p);v>b&&(v=b),d>_&&(d=_),b>y&&(y=b),_>m&&(m=_),s.push(b),h.push(_)}var w=y-v,S=m-d;w>S?m=d+w:y=v+S;var k=hr();if(k.add=function(n){u(k,n,+M(n,++p),+x(n,p),v,d,y,m)},k.visit=function(n){pr(n,k,v,d,y,m)},k.find=function(n){return gr(k,n[0],n[1],v,d,y,m)},p=-1,null==t){for(;++p=0?n.slice(0,t):n,r=t>=0?n.slice(t+1):"in";return e=vl.get(e)||gl,r=dl.get(r)||m,br(r(e.apply(null,lo.call(arguments,1))))},ao.interpolateHcl=Rr,ao.interpolateHsl=Dr,ao.interpolateLab=Pr,ao.interpolateRound=Ur,ao.transform=function(n){var t=fo.createElementNS(ao.ns.prefix.svg,"g");return(ao.transform=function(n){if(null!=n){t.setAttribute("transform",n);var e=t.transform.baseVal.consolidate()}return new jr(e?e.matrix:yl)})(n)},jr.prototype.toString=function(){return"translate("+this.translate+")rotate("+this.rotate+")skewX("+this.skew+")scale("+this.scale+")"};var yl={a:1,b:0,c:0,d:1,e:0,f:0};ao.interpolateTransform=$r,ao.layout={},ao.layout.bundle=function(){return function(n){for(var t=[],e=-1,r=n.length;++ea*a/y){if(v>l){var c=t.charge/l;n.px-=u*c,n.py-=o*c}return!0}if(t.point&&l&&v>l){var c=t.pointCharge/l;n.px-=u*c,n.py-=o*c}}return!t.charge}}function t(n){n.px=ao.event.x,n.py=ao.event.y,l.resume()}var e,r,i,u,o,a,l={},c=ao.dispatch("start","tick","end"),f=[1,1],s=.9,h=ml,p=Ml,g=-30,v=xl,d=.1,y=.64,M=[],x=[];return l.tick=function(){if((i*=.99)<.005)return e=null,c.end({type:"end",alpha:i=0}),!0;var t,r,l,h,p,v,y,m,b,_=M.length,w=x.length;for(r=0;w>r;++r)l=x[r],h=l.source,p=l.target,m=p.x-h.x,b=p.y-h.y,(v=m*m+b*b)&&(v=i*o[r]*((v=Math.sqrt(v))-u[r])/v,m*=v,b*=v,p.x-=m*(y=h.weight+p.weight?h.weight/(h.weight+p.weight):.5),p.y-=b*y,h.x+=m*(y=1-y),h.y+=b*y);if((y=i*d)&&(m=f[0]/2,b=f[1]/2,r=-1,y))for(;++r<_;)l=M[r],l.x+=(m-l.x)*y,l.y+=(b-l.y)*y;if(g)for(ri(t=ao.geom.quadtree(M),i,a),r=-1;++r<_;)(l=M[r]).fixed||t.visit(n(l));for(r=-1;++r<_;)l=M[r],l.fixed?(l.x=l.px,l.y=l.py):(l.x-=(l.px-(l.px=l.x))*s,l.y-=(l.py-(l.py=l.y))*s);c.tick({type:"tick",alpha:i})},l.nodes=function(n){return arguments.length?(M=n,l):M},l.links=function(n){return arguments.length?(x=n,l):x},l.size=function(n){return arguments.length?(f=n,l):f},l.linkDistance=function(n){return arguments.length?(h="function"==typeof n?n:+n,l):h},l.distance=l.linkDistance,l.linkStrength=function(n){return arguments.length?(p="function"==typeof n?n:+n,l):p},l.friction=function(n){return arguments.length?(s=+n,l):s},l.charge=function(n){return arguments.length?(g="function"==typeof n?n:+n,l):g},l.chargeDistance=function(n){return arguments.length?(v=n*n,l):Math.sqrt(v)},l.gravity=function(n){return arguments.length?(d=+n,l):d},l.theta=function(n){return arguments.length?(y=n*n,l):Math.sqrt(y)},l.alpha=function(n){return arguments.length?(n=+n,i?n>0?i=n:(e.c=null,e.t=NaN,e=null,c.end({type:"end",alpha:i=0})):n>0&&(c.start({type:"start",alpha:i=n}),e=qn(l.tick)),l):i},l.start=function(){function n(n,r){if(!e){for(e=new Array(i),l=0;i>l;++l)e[l]=[];for(l=0;c>l;++l){var u=x[l];e[u.source.index].push(u.target),e[u.target.index].push(u.source)}}for(var o,a=e[t],l=-1,f=a.length;++lt;++t)(r=M[t]).index=t,r.weight=0;for(t=0;c>t;++t)r=x[t],"number"==typeof r.source&&(r.source=M[r.source]),"number"==typeof r.target&&(r.target=M[r.target]),++r.source.weight,++r.target.weight;for(t=0;i>t;++t)r=M[t],isNaN(r.x)&&(r.x=n("x",s)),isNaN(r.y)&&(r.y=n("y",v)),isNaN(r.px)&&(r.px=r.x),isNaN(r.py)&&(r.py=r.y);if(u=[],"function"==typeof h)for(t=0;c>t;++t)u[t]=+h.call(this,x[t],t);else for(t=0;c>t;++t)u[t]=h;if(o=[],"function"==typeof p)for(t=0;c>t;++t)o[t]=+p.call(this,x[t],t);else for(t=0;c>t;++t)o[t]=p;if(a=[],"function"==typeof g)for(t=0;i>t;++t)a[t]=+g.call(this,M[t],t);else for(t=0;i>t;++t)a[t]=g;return l.resume()},l.resume=function(){return l.alpha(.1)},l.stop=function(){return l.alpha(0)},l.drag=function(){return r||(r=ao.behavior.drag().origin(m).on("dragstart.force",Qr).on("drag.force",t).on("dragend.force",ni)),arguments.length?void this.on("mouseover.force",ti).on("mouseout.force",ei).call(r):r},ao.rebind(l,c,"on")};var ml=20,Ml=1,xl=1/0;ao.layout.hierarchy=function(){function n(i){var u,o=[i],a=[];for(i.depth=0;null!=(u=o.pop());)if(a.push(u),(c=e.call(n,u,u.depth))&&(l=c.length)){for(var l,c,f;--l>=0;)o.push(f=c[l]),f.parent=u,f.depth=u.depth+1;r&&(u.value=0),u.children=c}else r&&(u.value=+r.call(n,u,u.depth)||0),delete u.children;return oi(i,function(n){var e,i;t&&(e=n.children)&&e.sort(t),r&&(i=n.parent)&&(i.value+=n.value)}),a}var t=ci,e=ai,r=li;return n.sort=function(e){return arguments.length?(t=e,n):t},n.children=function(t){return arguments.length?(e=t,n):e},n.value=function(t){return arguments.length?(r=t,n):r},n.revalue=function(t){return r&&(ui(t,function(n){n.children&&(n.value=0)}),oi(t,function(t){var e;t.children||(t.value=+r.call(n,t,t.depth)||0),(e=t.parent)&&(e.value+=t.value)})),t},n},ao.layout.partition=function(){function n(t,e,r,i){var u=t.children;if(t.x=e,t.y=t.depth*i,t.dx=r,t.dy=i,u&&(o=u.length)){var o,a,l,c=-1;for(r=t.value?r/t.value:0;++cs?-1:1),g=ao.sum(c),v=g?(s-l*p)/g:0,d=ao.range(l),y=[];return null!=e&&d.sort(e===bl?function(n,t){return c[t]-c[n]}:function(n,t){return e(o[n],o[t])}),d.forEach(function(n){y[n]={data:o[n],value:a=c[n],startAngle:f,endAngle:f+=a*v+p,padAngle:h}}),y}var t=Number,e=bl,r=0,i=Ho,u=0;return n.value=function(e){return arguments.length?(t=e,n):t},n.sort=function(t){return arguments.length?(e=t,n):e},n.startAngle=function(t){return arguments.length?(r=t,n):r},n.endAngle=function(t){return arguments.length?(i=t,n):i},n.padAngle=function(t){return arguments.length?(u=t,n):u},n};var bl={};ao.layout.stack=function(){function n(a,l){if(!(h=a.length))return a;var c=a.map(function(e,r){return t.call(n,e,r)}),f=c.map(function(t){return t.map(function(t,e){return[u.call(n,t,e),o.call(n,t,e)]})}),s=e.call(n,f,l);c=ao.permute(c,s),f=ao.permute(f,s);var h,p,g,v,d=r.call(n,f,l),y=c[0].length;for(g=0;y>g;++g)for(i.call(n,c[0][g],v=d[g],f[0][g][1]),p=1;h>p;++p)i.call(n,c[p][g],v+=f[p-1][g][1],f[p][g][1]);return a}var t=m,e=gi,r=vi,i=pi,u=si,o=hi;return n.values=function(e){return arguments.length?(t=e,n):t},n.order=function(t){return arguments.length?(e="function"==typeof t?t:_l.get(t)||gi,n):e},n.offset=function(t){return arguments.length?(r="function"==typeof t?t:wl.get(t)||vi,n):r},n.x=function(t){return arguments.length?(u=t,n):u},n.y=function(t){return arguments.length?(o=t,n):o},n.out=function(t){return arguments.length?(i=t,n):i},n};var _l=ao.map({"inside-out":function(n){var t,e,r=n.length,i=n.map(di),u=n.map(yi),o=ao.range(r).sort(function(n,t){return i[n]-i[t]}),a=0,l=0,c=[],f=[];for(t=0;r>t;++t)e=o[t],l>a?(a+=u[e],c.push(e)):(l+=u[e],f.push(e));return f.reverse().concat(c)},reverse:function(n){return ao.range(n.length).reverse()},"default":gi}),wl=ao.map({silhouette:function(n){var t,e,r,i=n.length,u=n[0].length,o=[],a=0,l=[];for(e=0;u>e;++e){for(t=0,r=0;i>t;t++)r+=n[t][e][1];r>a&&(a=r),o.push(r)}for(e=0;u>e;++e)l[e]=(a-o[e])/2;return l},wiggle:function(n){var t,e,r,i,u,o,a,l,c,f=n.length,s=n[0],h=s.length,p=[];for(p[0]=l=c=0,e=1;h>e;++e){for(t=0,i=0;f>t;++t)i+=n[t][e][1];for(t=0,u=0,a=s[e][0]-s[e-1][0];f>t;++t){for(r=0,o=(n[t][e][1]-n[t][e-1][1])/(2*a);t>r;++r)o+=(n[r][e][1]-n[r][e-1][1])/a;u+=o*n[t][e][1]}p[e]=l-=i?u/i*a:0,c>l&&(c=l)}for(e=0;h>e;++e)p[e]-=c;return p},expand:function(n){var t,e,r,i=n.length,u=n[0].length,o=1/i,a=[];for(e=0;u>e;++e){for(t=0,r=0;i>t;t++)r+=n[t][e][1];if(r)for(t=0;i>t;t++)n[t][e][1]/=r;else for(t=0;i>t;t++)n[t][e][1]=o}for(e=0;u>e;++e)a[e]=0;return a},zero:vi});ao.layout.histogram=function(){function n(n,u){for(var o,a,l=[],c=n.map(e,this),f=r.call(this,c,u),s=i.call(this,f,c,u),u=-1,h=c.length,p=s.length-1,g=t?1:1/h;++u0)for(u=-1;++u=f[0]&&a<=f[1]&&(o=l[ao.bisect(s,a,1,p)-1],o.y+=g,o.push(n[u]));return l}var t=!0,e=Number,r=bi,i=Mi;return n.value=function(t){return arguments.length?(e=t,n):e},n.range=function(t){return arguments.length?(r=En(t),n):r},n.bins=function(t){return arguments.length?(i="number"==typeof t?function(n){return xi(n,t)}:En(t),n):i},n.frequency=function(e){return arguments.length?(t=!!e,n):t},n},ao.layout.pack=function(){function n(n,u){var o=e.call(this,n,u),a=o[0],l=i[0],c=i[1],f=null==t?Math.sqrt:"function"==typeof t?t:function(){return t};if(a.x=a.y=0,oi(a,function(n){n.r=+f(n.value)}),oi(a,Ni),r){var s=r*(t?1:Math.max(2*a.r/l,2*a.r/c))/2;oi(a,function(n){n.r+=s}),oi(a,Ni),oi(a,function(n){n.r-=s})}return Ci(a,l/2,c/2,t?1:1/Math.max(2*a.r/l,2*a.r/c)),o}var t,e=ao.layout.hierarchy().sort(_i),r=0,i=[1,1];return n.size=function(t){return arguments.length?(i=t,n):i},n.radius=function(e){return arguments.length?(t=null==e||"function"==typeof e?e:+e,n):t},n.padding=function(t){return arguments.length?(r=+t,n):r},ii(n,e)},ao.layout.tree=function(){function n(n,i){var f=o.call(this,n,i),s=f[0],h=t(s);if(oi(h,e),h.parent.m=-h.z,ui(h,r),c)ui(s,u);else{var p=s,g=s,v=s;ui(s,function(n){n.xg.x&&(g=n),n.depth>v.depth&&(v=n)});var d=a(p,g)/2-p.x,y=l[0]/(g.x+a(g,p)/2+d),m=l[1]/(v.depth||1);ui(s,function(n){n.x=(n.x+d)*y,n.y=n.depth*m})}return f}function t(n){for(var t,e={A:null,children:[n]},r=[e];null!=(t=r.pop());)for(var i,u=t.children,o=0,a=u.length;a>o;++o)r.push((u[o]=i={_:u[o],parent:t,children:(i=u[o].children)&&i.slice()||[],A:null,a:null,z:0,m:0,c:0,s:0,t:null,i:o}).a=i);return e.children[0]}function e(n){var t=n.children,e=n.parent.children,r=n.i?e[n.i-1]:null;if(t.length){Di(n);var u=(t[0].z+t[t.length-1].z)/2;r?(n.z=r.z+a(n._,r._),n.m=n.z-u):n.z=u}else r&&(n.z=r.z+a(n._,r._));n.parent.A=i(n,r,n.parent.A||e[0])}function r(n){n._.x=n.z+n.parent.m,n.m+=n.parent.m}function i(n,t,e){if(t){for(var r,i=n,u=n,o=t,l=i.parent.children[0],c=i.m,f=u.m,s=o.m,h=l.m;o=Ti(o),i=qi(i),o&&i;)l=qi(l),u=Ti(u),u.a=n,r=o.z+s-i.z-c+a(o._,i._),r>0&&(Ri(Pi(o,n,e),n,r),c+=r,f+=r),s+=o.m,c+=i.m,h+=l.m,f+=u.m;o&&!Ti(u)&&(u.t=o,u.m+=s-f),i&&!qi(l)&&(l.t=i,l.m+=c-h,e=n)}return e}function u(n){n.x*=l[0],n.y=n.depth*l[1]}var o=ao.layout.hierarchy().sort(null).value(null),a=Li,l=[1,1],c=null;return n.separation=function(t){return arguments.length?(a=t,n):a},n.size=function(t){return arguments.length?(c=null==(l=t)?u:null,n):c?null:l},n.nodeSize=function(t){return arguments.length?(c=null==(l=t)?null:u,n):c?l:null},ii(n,o)},ao.layout.cluster=function(){function n(n,u){var o,a=t.call(this,n,u),l=a[0],c=0;oi(l,function(n){var t=n.children;t&&t.length?(n.x=ji(t),n.y=Ui(t)):(n.x=o?c+=e(n,o):0,n.y=0,o=n)});var f=Fi(l),s=Hi(l),h=f.x-e(f,s)/2,p=s.x+e(s,f)/2;return oi(l,i?function(n){n.x=(n.x-l.x)*r[0],n.y=(l.y-n.y)*r[1]}:function(n){n.x=(n.x-h)/(p-h)*r[0],n.y=(1-(l.y?n.y/l.y:1))*r[1]}),a}var t=ao.layout.hierarchy().sort(null).value(null),e=Li,r=[1,1],i=!1;return n.separation=function(t){return arguments.length?(e=t,n):e},n.size=function(t){return arguments.length?(i=null==(r=t),n):i?null:r},n.nodeSize=function(t){return arguments.length?(i=null!=(r=t),n):i?r:null},ii(n,t)},ao.layout.treemap=function(){function n(n,t){for(var e,r,i=-1,u=n.length;++it?0:t),e.area=isNaN(r)||0>=r?0:r}function t(e){var u=e.children;if(u&&u.length){var o,a,l,c=s(e),f=[],h=u.slice(),g=1/0,v="slice"===p?c.dx:"dice"===p?c.dy:"slice-dice"===p?1&e.depth?c.dy:c.dx:Math.min(c.dx,c.dy);for(n(h,c.dx*c.dy/e.value),f.area=0;(l=h.length)>0;)f.push(o=h[l-1]),f.area+=o.area,"squarify"!==p||(a=r(f,v))<=g?(h.pop(),g=a):(f.area-=f.pop().area,i(f,v,c,!1),v=Math.min(c.dx,c.dy),f.length=f.area=0,g=1/0);f.length&&(i(f,v,c,!0),f.length=f.area=0),u.forEach(t)}}function e(t){var r=t.children;if(r&&r.length){var u,o=s(t),a=r.slice(),l=[];for(n(a,o.dx*o.dy/t.value),l.area=0;u=a.pop();)l.push(u),l.area+=u.area,null!=u.z&&(i(l,u.z?o.dx:o.dy,o,!a.length),l.length=l.area=0);r.forEach(e)}}function r(n,t){for(var e,r=n.area,i=0,u=1/0,o=-1,a=n.length;++oe&&(u=e),e>i&&(i=e));return r*=r,t*=t,r?Math.max(t*i*g/r,r/(t*u*g)):1/0}function i(n,t,e,r){var i,u=-1,o=n.length,a=e.x,c=e.y,f=t?l(n.area/t):0;
+if(t==e.dx){for((r||f>e.dy)&&(f=e.dy);++ue.dx)&&(f=e.dx);++ue&&(t=1),1>e&&(n=0),function(){var e,r,i;do e=2*Math.random()-1,r=2*Math.random()-1,i=e*e+r*r;while(!i||i>1);return n+t*e*Math.sqrt(-2*Math.log(i)/i)}},logNormal:function(){var n=ao.random.normal.apply(ao,arguments);return function(){return Math.exp(n())}},bates:function(n){var t=ao.random.irwinHall(n);return function(){return t()/n}},irwinHall:function(n){return function(){for(var t=0,e=0;n>e;e++)t+=Math.random();return t}}},ao.scale={};var Sl={floor:m,ceil:m};ao.scale.linear=function(){return Wi([0,1],[0,1],Mr,!1)};var kl={s:1,g:1,p:1,r:1,e:1};ao.scale.log=function(){return ru(ao.scale.linear().domain([0,1]),10,!0,[1,10])};var Nl=ao.format(".0e"),El={floor:function(n){return-Math.ceil(-n)},ceil:function(n){return-Math.floor(-n)}};ao.scale.pow=function(){return iu(ao.scale.linear(),1,[0,1])},ao.scale.sqrt=function(){return ao.scale.pow().exponent(.5)},ao.scale.ordinal=function(){return ou([],{t:"range",a:[[]]})},ao.scale.category10=function(){return ao.scale.ordinal().range(Al)},ao.scale.category20=function(){return ao.scale.ordinal().range(Cl)},ao.scale.category20b=function(){return ao.scale.ordinal().range(zl)},ao.scale.category20c=function(){return ao.scale.ordinal().range(Ll)};var Al=[2062260,16744206,2924588,14034728,9725885,9197131,14907330,8355711,12369186,1556175].map(xn),Cl=[2062260,11454440,16744206,16759672,2924588,10018698,14034728,16750742,9725885,12955861,9197131,12885140,14907330,16234194,8355711,13092807,12369186,14408589,1556175,10410725].map(xn),zl=[3750777,5395619,7040719,10264286,6519097,9216594,11915115,13556636,9202993,12426809,15186514,15190932,8666169,11356490,14049643,15177372,8077683,10834324,13528509,14589654].map(xn),Ll=[3244733,7057110,10406625,13032431,15095053,16616764,16625259,16634018,3253076,7652470,10607003,13101504,7695281,10394312,12369372,14342891,6513507,9868950,12434877,14277081].map(xn);ao.scale.quantile=function(){return au([],[])},ao.scale.quantize=function(){return lu(0,1,[0,1])},ao.scale.threshold=function(){return cu([.5],[0,1])},ao.scale.identity=function(){return fu([0,1])},ao.svg={},ao.svg.arc=function(){function n(){var n=Math.max(0,+e.apply(this,arguments)),c=Math.max(0,+r.apply(this,arguments)),f=o.apply(this,arguments)-Io,s=a.apply(this,arguments)-Io,h=Math.abs(s-f),p=f>s?0:1;if(n>c&&(g=c,c=n,n=g),h>=Oo)return t(c,p)+(n?t(n,1-p):"")+"Z";var g,v,d,y,m,M,x,b,_,w,S,k,N=0,E=0,A=[];if((y=(+l.apply(this,arguments)||0)/2)&&(d=u===ql?Math.sqrt(n*n+c*c):+u.apply(this,arguments),p||(E*=-1),c&&(E=tn(d/c*Math.sin(y))),n&&(N=tn(d/n*Math.sin(y)))),c){m=c*Math.cos(f+E),M=c*Math.sin(f+E),x=c*Math.cos(s-E),b=c*Math.sin(s-E);var C=Math.abs(s-f-2*E)<=Fo?0:1;if(E&&yu(m,M,x,b)===p^C){var z=(f+s)/2;m=c*Math.cos(z),M=c*Math.sin(z),x=b=null}}else m=M=0;if(n){_=n*Math.cos(s-N),w=n*Math.sin(s-N),S=n*Math.cos(f+N),k=n*Math.sin(f+N);var L=Math.abs(f-s+2*N)<=Fo?0:1;if(N&&yu(_,w,S,k)===1-p^L){var q=(f+s)/2;_=n*Math.cos(q),w=n*Math.sin(q),S=k=null}}else _=w=0;if(h>Uo&&(g=Math.min(Math.abs(c-n)/2,+i.apply(this,arguments)))>.001){v=c>n^p?0:1;var T=g,R=g;if(Fo>h){var D=null==S?[_,w]:null==x?[m,M]:Re([m,M],[S,k],[x,b],[_,w]),P=m-D[0],U=M-D[1],j=x-D[0],F=b-D[1],H=1/Math.sin(Math.acos((P*j+U*F)/(Math.sqrt(P*P+U*U)*Math.sqrt(j*j+F*F)))/2),O=Math.sqrt(D[0]*D[0]+D[1]*D[1]);R=Math.min(g,(n-O)/(H-1)),T=Math.min(g,(c-O)/(H+1))}if(null!=x){var I=mu(null==S?[_,w]:[S,k],[m,M],c,T,p),Y=mu([x,b],[_,w],c,T,p);g===T?A.push("M",I[0],"A",T,",",T," 0 0,",v," ",I[1],"A",c,",",c," 0 ",1-p^yu(I[1][0],I[1][1],Y[1][0],Y[1][1]),",",p," ",Y[1],"A",T,",",T," 0 0,",v," ",Y[0]):A.push("M",I[0],"A",T,",",T," 0 1,",v," ",Y[0])}else A.push("M",m,",",M);if(null!=S){var Z=mu([m,M],[S,k],n,-R,p),V=mu([_,w],null==x?[m,M]:[x,b],n,-R,p);g===R?A.push("L",V[0],"A",R,",",R," 0 0,",v," ",V[1],"A",n,",",n," 0 ",p^yu(V[1][0],V[1][1],Z[1][0],Z[1][1]),",",1-p," ",Z[1],"A",R,",",R," 0 0,",v," ",Z[0]):A.push("L",V[0],"A",R,",",R," 0 0,",v," ",Z[0])}else A.push("L",_,",",w)}else A.push("M",m,",",M),null!=x&&A.push("A",c,",",c," 0 ",C,",",p," ",x,",",b),A.push("L",_,",",w),null!=S&&A.push("A",n,",",n," 0 ",L,",",1-p," ",S,",",k);return A.push("Z"),A.join("")}function t(n,t){return"M0,"+n+"A"+n+","+n+" 0 1,"+t+" 0,"+-n+"A"+n+","+n+" 0 1,"+t+" 0,"+n}var e=hu,r=pu,i=su,u=ql,o=gu,a=vu,l=du;return n.innerRadius=function(t){return arguments.length?(e=En(t),n):e},n.outerRadius=function(t){return arguments.length?(r=En(t),n):r},n.cornerRadius=function(t){return arguments.length?(i=En(t),n):i},n.padRadius=function(t){return arguments.length?(u=t==ql?ql:En(t),n):u},n.startAngle=function(t){return arguments.length?(o=En(t),n):o},n.endAngle=function(t){return arguments.length?(a=En(t),n):a},n.padAngle=function(t){return arguments.length?(l=En(t),n):l},n.centroid=function(){var n=(+e.apply(this,arguments)+ +r.apply(this,arguments))/2,t=(+o.apply(this,arguments)+ +a.apply(this,arguments))/2-Io;return[Math.cos(t)*n,Math.sin(t)*n]},n};var ql="auto";ao.svg.line=function(){return Mu(m)};var Tl=ao.map({linear:xu,"linear-closed":bu,step:_u,"step-before":wu,"step-after":Su,basis:zu,"basis-open":Lu,"basis-closed":qu,bundle:Tu,cardinal:Eu,"cardinal-open":ku,"cardinal-closed":Nu,monotone:Fu});Tl.forEach(function(n,t){t.key=n,t.closed=/-closed$/.test(n)});var Rl=[0,2/3,1/3,0],Dl=[0,1/3,2/3,0],Pl=[0,1/6,2/3,1/6];ao.svg.line.radial=function(){var n=Mu(Hu);return n.radius=n.x,delete n.x,n.angle=n.y,delete n.y,n},wu.reverse=Su,Su.reverse=wu,ao.svg.area=function(){return Ou(m)},ao.svg.area.radial=function(){var n=Ou(Hu);return n.radius=n.x,delete n.x,n.innerRadius=n.x0,delete n.x0,n.outerRadius=n.x1,delete n.x1,n.angle=n.y,delete n.y,n.startAngle=n.y0,delete n.y0,n.endAngle=n.y1,delete n.y1,n},ao.svg.chord=function(){function n(n,a){var l=t(this,u,n,a),c=t(this,o,n,a);return"M"+l.p0+r(l.r,l.p1,l.a1-l.a0)+(e(l,c)?i(l.r,l.p1,l.r,l.p0):i(l.r,l.p1,c.r,c.p0)+r(c.r,c.p1,c.a1-c.a0)+i(c.r,c.p1,l.r,l.p0))+"Z"}function t(n,t,e,r){var i=t.call(n,e,r),u=a.call(n,i,r),o=l.call(n,i,r)-Io,f=c.call(n,i,r)-Io;return{r:u,a0:o,a1:f,p0:[u*Math.cos(o),u*Math.sin(o)],p1:[u*Math.cos(f),u*Math.sin(f)]}}function e(n,t){return n.a0==t.a0&&n.a1==t.a1}function r(n,t,e){return"A"+n+","+n+" 0 "+ +(e>Fo)+",1 "+t}function i(n,t,e,r){return"Q 0,0 "+r}var u=Me,o=xe,a=Iu,l=gu,c=vu;return n.radius=function(t){return arguments.length?(a=En(t),n):a},n.source=function(t){return arguments.length?(u=En(t),n):u},n.target=function(t){return arguments.length?(o=En(t),n):o},n.startAngle=function(t){return arguments.length?(l=En(t),n):l},n.endAngle=function(t){return arguments.length?(c=En(t),n):c},n},ao.svg.diagonal=function(){function n(n,i){var u=t.call(this,n,i),o=e.call(this,n,i),a=(u.y+o.y)/2,l=[u,{x:u.x,y:a},{x:o.x,y:a},o];return l=l.map(r),"M"+l[0]+"C"+l[1]+" "+l[2]+" "+l[3]}var t=Me,e=xe,r=Yu;return n.source=function(e){return arguments.length?(t=En(e),n):t},n.target=function(t){return arguments.length?(e=En(t),n):e},n.projection=function(t){return arguments.length?(r=t,n):r},n},ao.svg.diagonal.radial=function(){var n=ao.svg.diagonal(),t=Yu,e=n.projection;return n.projection=function(n){return arguments.length?e(Zu(t=n)):t},n},ao.svg.symbol=function(){function n(n,r){return(Ul.get(t.call(this,n,r))||$u)(e.call(this,n,r))}var t=Xu,e=Vu;return n.type=function(e){return arguments.length?(t=En(e),n):t},n.size=function(t){return arguments.length?(e=En(t),n):e},n};var Ul=ao.map({circle:$u,cross:function(n){var t=Math.sqrt(n/5)/2;return"M"+-3*t+","+-t+"H"+-t+"V"+-3*t+"H"+t+"V"+-t+"H"+3*t+"V"+t+"H"+t+"V"+3*t+"H"+-t+"V"+t+"H"+-3*t+"Z"},diamond:function(n){var t=Math.sqrt(n/(2*Fl)),e=t*Fl;return"M0,"+-t+"L"+e+",0 0,"+t+" "+-e+",0Z"},square:function(n){var t=Math.sqrt(n)/2;return"M"+-t+","+-t+"L"+t+","+-t+" "+t+","+t+" "+-t+","+t+"Z"},"triangle-down":function(n){var t=Math.sqrt(n/jl),e=t*jl/2;return"M0,"+e+"L"+t+","+-e+" "+-t+","+-e+"Z"},"triangle-up":function(n){var t=Math.sqrt(n/jl),e=t*jl/2;return"M0,"+-e+"L"+t+","+e+" "+-t+","+e+"Z"}});ao.svg.symbolTypes=Ul.keys();var jl=Math.sqrt(3),Fl=Math.tan(30*Yo);Co.transition=function(n){for(var t,e,r=Hl||++Zl,i=Ku(n),u=[],o=Ol||{time:Date.now(),ease:Nr,delay:0,duration:250},a=-1,l=this.length;++au;u++){i.push(t=[]);for(var e=this[u],a=0,l=e.length;l>a;a++)(r=e[a])&&n.call(r,r.__data__,a,u)&&t.push(r)}return Wu(i,this.namespace,this.id)},Yl.tween=function(n,t){var e=this.id,r=this.namespace;return arguments.length<2?this.node()[r][e].tween.get(n):Y(this,null==t?function(t){t[r][e].tween.remove(n)}:function(i){i[r][e].tween.set(n,t)})},Yl.attr=function(n,t){function e(){this.removeAttribute(a)}function r(){this.removeAttributeNS(a.space,a.local)}function i(n){return null==n?e:(n+="",function(){var t,e=this.getAttribute(a);return e!==n&&(t=o(e,n),function(n){this.setAttribute(a,t(n))})})}function u(n){return null==n?r:(n+="",function(){var t,e=this.getAttributeNS(a.space,a.local);return e!==n&&(t=o(e,n),function(n){this.setAttributeNS(a.space,a.local,t(n))})})}if(arguments.length<2){for(t in n)this.attr(t,n[t]);return this}var o="transform"==n?$r:Mr,a=ao.ns.qualify(n);return Ju(this,"attr."+n,t,a.local?u:i)},Yl.attrTween=function(n,t){function e(n,e){var r=t.call(this,n,e,this.getAttribute(i));return r&&function(n){this.setAttribute(i,r(n))}}function r(n,e){var r=t.call(this,n,e,this.getAttributeNS(i.space,i.local));return r&&function(n){this.setAttributeNS(i.space,i.local,r(n))}}var i=ao.ns.qualify(n);return this.tween("attr."+n,i.local?r:e)},Yl.style=function(n,e,r){function i(){this.style.removeProperty(n)}function u(e){return null==e?i:(e+="",function(){var i,u=t(this).getComputedStyle(this,null).getPropertyValue(n);return u!==e&&(i=Mr(u,e),function(t){this.style.setProperty(n,i(t),r)})})}var o=arguments.length;if(3>o){if("string"!=typeof n){2>o&&(e="");for(r in n)this.style(r,n[r],e);return this}r=""}return Ju(this,"style."+n,e,u)},Yl.styleTween=function(n,e,r){function i(i,u){var o=e.call(this,i,u,t(this).getComputedStyle(this,null).getPropertyValue(n));return o&&function(t){this.style.setProperty(n,o(t),r)}}return arguments.length<3&&(r=""),this.tween("style."+n,i)},Yl.text=function(n){return Ju(this,"text",n,Gu)},Yl.remove=function(){var n=this.namespace;return this.each("end.transition",function(){var t;this[n].count<2&&(t=this.parentNode)&&t.removeChild(this)})},Yl.ease=function(n){var t=this.id,e=this.namespace;return arguments.length<1?this.node()[e][t].ease:("function"!=typeof n&&(n=ao.ease.apply(ao,arguments)),Y(this,function(r){r[e][t].ease=n}))},Yl.delay=function(n){var t=this.id,e=this.namespace;return arguments.length<1?this.node()[e][t].delay:Y(this,"function"==typeof n?function(r,i,u){r[e][t].delay=+n.call(r,r.__data__,i,u)}:(n=+n,function(r){r[e][t].delay=n}))},Yl.duration=function(n){var t=this.id,e=this.namespace;return arguments.length<1?this.node()[e][t].duration:Y(this,"function"==typeof n?function(r,i,u){r[e][t].duration=Math.max(1,n.call(r,r.__data__,i,u))}:(n=Math.max(1,n),function(r){r[e][t].duration=n}))},Yl.each=function(n,t){var e=this.id,r=this.namespace;if(arguments.length<2){var i=Ol,u=Hl;try{Hl=e,Y(this,function(t,i,u){Ol=t[r][e],n.call(t,t.__data__,i,u)})}finally{Ol=i,Hl=u}}else Y(this,function(i){var u=i[r][e];(u.event||(u.event=ao.dispatch("start","end","interrupt"))).on(n,t)});return this},Yl.transition=function(){for(var n,t,e,r,i=this.id,u=++Zl,o=this.namespace,a=[],l=0,c=this.length;c>l;l++){a.push(n=[]);for(var t=this[l],f=0,s=t.length;s>f;f++)(e=t[f])&&(r=e[o][i],Qu(e,f,o,u,{time:r.time,ease:r.ease,delay:r.delay+r.duration,duration:r.duration})),n.push(e)}return Wu(a,o,u)},ao.svg.axis=function(){function n(n){n.each(function(){var n,c=ao.select(this),f=this.__chart__||e,s=this.__chart__=e.copy(),h=null==l?s.ticks?s.ticks.apply(s,a):s.domain():l,p=null==t?s.tickFormat?s.tickFormat.apply(s,a):m:t,g=c.selectAll(".tick").data(h,s),v=g.enter().insert("g",".domain").attr("class","tick").style("opacity",Uo),d=ao.transition(g.exit()).style("opacity",Uo).remove(),y=ao.transition(g.order()).style("opacity",1),M=Math.max(i,0)+o,x=Zi(s),b=c.selectAll(".domain").data([0]),_=(b.enter().append("path").attr("class","domain"),ao.transition(b));v.append("line"),v.append("text");var w,S,k,N,E=v.select("line"),A=y.select("line"),C=g.select("text").text(p),z=v.select("text"),L=y.select("text"),q="top"===r||"left"===r?-1:1;if("bottom"===r||"top"===r?(n=no,w="x",k="y",S="x2",N="y2",C.attr("dy",0>q?"0em":".71em").style("text-anchor","middle"),_.attr("d","M"+x[0]+","+q*u+"V0H"+x[1]+"V"+q*u)):(n=to,w="y",k="x",S="y2",N="x2",C.attr("dy",".32em").style("text-anchor",0>q?"end":"start"),_.attr("d","M"+q*u+","+x[0]+"H0V"+x[1]+"H"+q*u)),E.attr(N,q*i),z.attr(k,q*M),A.attr(S,0).attr(N,q*i),L.attr(w,0).attr(k,q*M),s.rangeBand){var T=s,R=T.rangeBand()/2;f=s=function(n){return T(n)+R}}else f.rangeBand?f=s:d.call(n,s,f);v.call(n,f,s),y.call(n,s,s)})}var t,e=ao.scale.linear(),r=Vl,i=6,u=6,o=3,a=[10],l=null;return n.scale=function(t){return arguments.length?(e=t,n):e},n.orient=function(t){return arguments.length?(r=t in Xl?t+"":Vl,n):r},n.ticks=function(){return arguments.length?(a=co(arguments),n):a},n.tickValues=function(t){return arguments.length?(l=t,n):l},n.tickFormat=function(e){return arguments.length?(t=e,n):t},n.tickSize=function(t){var e=arguments.length;return e?(i=+t,u=+arguments[e-1],n):i},n.innerTickSize=function(t){return arguments.length?(i=+t,n):i},n.outerTickSize=function(t){return arguments.length?(u=+t,n):u},n.tickPadding=function(t){return arguments.length?(o=+t,n):o},n.tickSubdivide=function(){return arguments.length&&n},n};var Vl="bottom",Xl={top:1,right:1,bottom:1,left:1};ao.svg.brush=function(){function n(t){t.each(function(){var t=ao.select(this).style("pointer-events","all").style("-webkit-tap-highlight-color","rgba(0,0,0,0)").on("mousedown.brush",u).on("touchstart.brush",u),o=t.selectAll(".background").data([0]);o.enter().append("rect").attr("class","background").style("visibility","hidden").style("cursor","crosshair"),t.selectAll(".extent").data([0]).enter().append("rect").attr("class","extent").style("cursor","move");var a=t.selectAll(".resize").data(v,m);a.exit().remove(),a.enter().append("g").attr("class",function(n){return"resize "+n}).style("cursor",function(n){return $l[n]}).append("rect").attr("x",function(n){return/[ew]$/.test(n)?-3:null}).attr("y",function(n){return/^[ns]/.test(n)?-3:null}).attr("width",6).attr("height",6).style("visibility","hidden"),a.style("display",n.empty()?"none":null);var l,s=ao.transition(t),h=ao.transition(o);c&&(l=Zi(c),h.attr("x",l[0]).attr("width",l[1]-l[0]),r(s)),f&&(l=Zi(f),h.attr("y",l[0]).attr("height",l[1]-l[0]),i(s)),e(s)})}function e(n){n.selectAll(".resize").attr("transform",function(n){return"translate("+s[+/e$/.test(n)]+","+h[+/^s/.test(n)]+")"})}function r(n){n.select(".extent").attr("x",s[0]),n.selectAll(".extent,.n>rect,.s>rect").attr("width",s[1]-s[0])}function i(n){n.select(".extent").attr("y",h[0]),n.selectAll(".extent,.e>rect,.w>rect").attr("height",h[1]-h[0])}function u(){function u(){32==ao.event.keyCode&&(C||(M=null,L[0]-=s[1],L[1]-=h[1],C=2),S())}function v(){32==ao.event.keyCode&&2==C&&(L[0]+=s[1],L[1]+=h[1],C=0,S())}function d(){var n=ao.mouse(b),t=!1;x&&(n[0]+=x[0],n[1]+=x[1]),C||(ao.event.altKey?(M||(M=[(s[0]+s[1])/2,(h[0]+h[1])/2]),L[0]=s[+(n[0]f?(i=r,r=f):i=f),v[0]!=r||v[1]!=i?(e?a=null:o=null,v[0]=r,v[1]=i,!0):void 0}function m(){d(),k.style("pointer-events","all").selectAll(".resize").style("display",n.empty()?"none":null),ao.select("body").style("cursor",null),q.on("mousemove.brush",null).on("mouseup.brush",null).on("touchmove.brush",null).on("touchend.brush",null).on("keydown.brush",null).on("keyup.brush",null),z(),w({type:"brushend"})}var M,x,b=this,_=ao.select(ao.event.target),w=l.of(b,arguments),k=ao.select(b),N=_.datum(),E=!/^(n|s)$/.test(N)&&c,A=!/^(e|w)$/.test(N)&&f,C=_.classed("extent"),z=W(b),L=ao.mouse(b),q=ao.select(t(b)).on("keydown.brush",u).on("keyup.brush",v);if(ao.event.changedTouches?q.on("touchmove.brush",d).on("touchend.brush",m):q.on("mousemove.brush",d).on("mouseup.brush",m),k.interrupt().selectAll("*").interrupt(),C)L[0]=s[0]-L[0],L[1]=h[0]-L[1];else if(N){var T=+/w$/.test(N),R=+/^n/.test(N);x=[s[1-T]-L[0],h[1-R]-L[1]],L[0]=s[T],L[1]=h[R]}else ao.event.altKey&&(M=L.slice());k.style("pointer-events","none").selectAll(".resize").style("display",null),ao.select("body").style("cursor",_.style("cursor")),w({type:"brushstart"}),d()}var o,a,l=N(n,"brushstart","brush","brushend"),c=null,f=null,s=[0,0],h=[0,0],p=!0,g=!0,v=Bl[0];return n.event=function(n){n.each(function(){var n=l.of(this,arguments),t={x:s,y:h,i:o,j:a},e=this.__chart__||t;this.__chart__=t,Hl?ao.select(this).transition().each("start.brush",function(){o=e.i,a=e.j,s=e.x,h=e.y,n({type:"brushstart"})}).tween("brush:brush",function(){var e=xr(s,t.x),r=xr(h,t.y);return o=a=null,function(i){s=t.x=e(i),h=t.y=r(i),n({type:"brush",mode:"resize"})}}).each("end.brush",function(){o=t.i,a=t.j,n({type:"brush",mode:"resize"}),n({type:"brushend"})}):(n({type:"brushstart"}),n({type:"brush",mode:"resize"}),n({type:"brushend"}))})},n.x=function(t){return arguments.length?(c=t,v=Bl[!c<<1|!f],n):c},n.y=function(t){return arguments.length?(f=t,v=Bl[!c<<1|!f],n):f},n.clamp=function(t){return arguments.length?(c&&f?(p=!!t[0],g=!!t[1]):c?p=!!t:f&&(g=!!t),n):c&&f?[p,g]:c?p:f?g:null},n.extent=function(t){var e,r,i,u,l;return arguments.length?(c&&(e=t[0],r=t[1],f&&(e=e[0],r=r[0]),o=[e,r],c.invert&&(e=c(e),r=c(r)),e>r&&(l=e,e=r,r=l),e==s[0]&&r==s[1]||(s=[e,r])),f&&(i=t[0],u=t[1],c&&(i=i[1],u=u[1]),a=[i,u],f.invert&&(i=f(i),u=f(u)),i>u&&(l=i,i=u,u=l),i==h[0]&&u==h[1]||(h=[i,u])),n):(c&&(o?(e=o[0],r=o[1]):(e=s[0],r=s[1],c.invert&&(e=c.invert(e),r=c.invert(r)),e>r&&(l=e,e=r,r=l))),f&&(a?(i=a[0],u=a[1]):(i=h[0],u=h[1],f.invert&&(i=f.invert(i),u=f.invert(u)),i>u&&(l=i,i=u,u=l))),c&&f?[[e,i],[r,u]]:c?[e,r]:f&&[i,u])},n.clear=function(){return n.empty()||(s=[0,0],h=[0,0],o=a=null),n},n.empty=function(){return!!c&&s[0]==s[1]||!!f&&h[0]==h[1]},ao.rebind(n,l,"on")};var $l={n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},Bl=[["n","e","s","w","nw","ne","se","sw"],["e","w"],["n","s"],[]],Wl=ga.format=xa.timeFormat,Jl=Wl.utc,Gl=Jl("%Y-%m-%dT%H:%M:%S.%LZ");Wl.iso=Date.prototype.toISOString&&+new Date("2000-01-01T00:00:00.000Z")?eo:Gl,eo.parse=function(n){var t=new Date(n);return isNaN(t)?null:t},eo.toString=Gl.toString,ga.second=On(function(n){return new va(1e3*Math.floor(n/1e3))},function(n,t){n.setTime(n.getTime()+1e3*Math.floor(t))},function(n){return n.getSeconds()}),ga.seconds=ga.second.range,ga.seconds.utc=ga.second.utc.range,ga.minute=On(function(n){return new va(6e4*Math.floor(n/6e4))},function(n,t){n.setTime(n.getTime()+6e4*Math.floor(t))},function(n){return n.getMinutes()}),ga.minutes=ga.minute.range,ga.minutes.utc=ga.minute.utc.range,ga.hour=On(function(n){var t=n.getTimezoneOffset()/60;return new va(36e5*(Math.floor(n/36e5-t)+t))},function(n,t){n.setTime(n.getTime()+36e5*Math.floor(t))},function(n){return n.getHours()}),ga.hours=ga.hour.range,ga.hours.utc=ga.hour.utc.range,ga.month=On(function(n){return n=ga.day(n),n.setDate(1),n},function(n,t){n.setMonth(n.getMonth()+t)},function(n){return n.getMonth()}),ga.months=ga.month.range,ga.months.utc=ga.month.utc.range;var Kl=[1e3,5e3,15e3,3e4,6e4,3e5,9e5,18e5,36e5,108e5,216e5,432e5,864e5,1728e5,6048e5,2592e6,7776e6,31536e6],Ql=[[ga.second,1],[ga.second,5],[ga.second,15],[ga.second,30],[ga.minute,1],[ga.minute,5],[ga.minute,15],[ga.minute,30],[ga.hour,1],[ga.hour,3],[ga.hour,6],[ga.hour,12],[ga.day,1],[ga.day,2],[ga.week,1],[ga.month,1],[ga.month,3],[ga.year,1]],nc=Wl.multi([[".%L",function(n){return n.getMilliseconds()}],[":%S",function(n){return n.getSeconds()}],["%I:%M",function(n){return n.getMinutes()}],["%I %p",function(n){return n.getHours()}],["%a %d",function(n){return n.getDay()&&1!=n.getDate()}],["%b %d",function(n){return 1!=n.getDate()}],["%B",function(n){return n.getMonth()}],["%Y",zt]]),tc={range:function(n,t,e){return ao.range(Math.ceil(n/e)*e,+t,e).map(io)},floor:m,ceil:m};Ql.year=ga.year,ga.scale=function(){return ro(ao.scale.linear(),Ql,nc)};var ec=Ql.map(function(n){return[n[0].utc,n[1]]}),rc=Jl.multi([[".%L",function(n){return n.getUTCMilliseconds()}],[":%S",function(n){return n.getUTCSeconds()}],["%I:%M",function(n){return n.getUTCMinutes()}],["%I %p",function(n){return n.getUTCHours()}],["%a %d",function(n){return n.getUTCDay()&&1!=n.getUTCDate()}],["%b %d",function(n){return 1!=n.getUTCDate()}],["%B",function(n){return n.getUTCMonth()}],["%Y",zt]]);ec.year=ga.year.utc,ga.scale.utc=function(){return ro(ao.scale.linear(),ec,rc)},ao.text=An(function(n){return n.responseText}),ao.json=function(n,t){return Cn(n,"application/json",uo,t)},ao.html=function(n,t){return Cn(n,"text/html",oo,t)},ao.xml=An(function(n){return n.responseXML}),"function"==typeof define&&define.amd?(this.d3=ao,define(ao)):"object"==typeof module&&module.exports?module.exports=ao:this.d3=ao}();
\ No newline at end of file
diff --git a/admin/static/js/i18n b/admin/static/js/i18n
deleted file mode 120000
index dab94408..00000000
--- a/admin/static/js/i18n
+++ /dev/null
@@ -1 +0,0 @@
-../../../frontend/static/js/i18n/
\ No newline at end of file
diff --git a/frontend/static/js/i18n/angular-locale_fr-fr.js b/admin/static/js/i18n/angular-locale_fr-fr.js
similarity index 90%
rename from frontend/static/js/i18n/angular-locale_fr-fr.js
rename to admin/static/js/i18n/angular-locale_fr-fr.js
index 745a956b..2e6dbbbb 100644
--- a/frontend/static/js/i18n/angular-locale_fr-fr.js
+++ b/admin/static/js/i18n/angular-locale_fr-fr.js
@@ -63,18 +63,18 @@ $provide.value("$locale", {
"d\u00e9c."
],
"STANDALONEMONTH": [
- "Janvier",
- "F\u00e9vrier",
- "Mars",
- "Avril",
- "Mai",
- "Juin",
- "Juillet",
- "Ao\u00fbt",
- "Septembre",
- "Octobre",
- "Novembre",
- "D\u00e9cembre"
+ "janvier",
+ "f\u00e9vrier",
+ "mars",
+ "avril",
+ "mai",
+ "juin",
+ "juillet",
+ "ao\u00fbt",
+ "septembre",
+ "octobre",
+ "novembre",
+ "d\u00e9cembre"
],
"WEEKENDRANGE": [
5,
@@ -119,6 +119,7 @@ $provide.value("$locale", {
]
},
"id": "fr-fr",
+ "localeID": "fr_FR",
"pluralCat": function(n, opt_precision) { var i = n | 0; if (i == 0 || i == 1) { return PLURAL_CATEGORY.ONE; } return PLURAL_CATEGORY.OTHER;}
});
}]);
diff --git a/admin/static/js/jquery.min.js b/admin/static/js/jquery.min.js
deleted file mode 120000
index 022463c1..00000000
--- a/admin/static/js/jquery.min.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../frontend/static/js/jquery.min.js
\ No newline at end of file
diff --git a/admin/static/js/jquery.min.js b/admin/static/js/jquery.min.js
new file mode 100644
index 00000000..36b4e1a1
--- /dev/null
+++ b/admin/static/js/jquery.min.js
@@ -0,0 +1,2 @@
+/*! jQuery v3.5.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/Tween,-effects/animatedSelector | (c) JS Foundation and other contributors | jquery.org/license */
+!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(g,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,v=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,y=n.hasOwnProperty,a=y.toString,l=a.call(Object),m={},b=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},x=function(e){return null!=e&&e===e.window},w=g.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function C(e,t,n){var r,i,o=(n=n||w).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function T(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.5.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/Tween,-effects/animatedSelector",E=function(e,t){return new E.fn.init(e,t)};function d(e){var t=!!e&&"length"in e&&e.length,n=T(e);return!b(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+R+")"+R+"*"),U=new RegExp(R+"|>"),V=new RegExp(W),X=new RegExp("^"+B+"$"),Q={ID:new RegExp("^#("+B+")"),CLASS:new RegExp("^\\.("+B+")"),TAG:new RegExp("^("+B+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+W),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+R+"*(even|odd|(([+-]|)(\\d*)n|)"+R+"*(?:([+-]|)"+R+"*(\\d+)|))"+R+"*\\)|)","i"),bool:new RegExp("^(?:"+I+")$","i"),needsContext:new RegExp("^"+R+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+R+"*((?:-\\d)?\\d*)"+R+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,G=/^(?:input|select|textarea|button)$/i,K=/^h\d$/i,J=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+R+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){C()},ae=xe(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{O.apply(t=P.call(d.childNodes),d.childNodes),t[d.childNodes.length].nodeType}catch(e){O={apply:t.length?function(e,t){q.apply(e,P.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,d=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==d&&9!==d&&11!==d)return n;if(!r&&(C(e),e=e||T,E)){if(11!==d&&(u=Z.exec(t)))if(i=u[1]){if(9===d){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return O.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&p.getElementsByClassName&&e.getElementsByClassName)return O.apply(n,e.getElementsByClassName(i)),n}if(p.qsa&&!k[t+" "]&&(!v||!v.test(t))&&(1!==d||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===d&&(U.test(t)||_.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&p.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=A)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+be(l[o]);c=l.join(",")}try{return O.apply(n,f.querySelectorAll(c)),n}catch(e){k(t,!0)}finally{s===A&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>x.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[A]=!0,e}function ce(e){var t=T.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)x.attrHandle[n[r]]=t}function de(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function pe(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in p=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},C=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:d;return r!=T&&9===r.nodeType&&r.documentElement&&(a=(T=r).documentElement,E=!i(T),d!=T&&(n=T.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),p.scope=ce(function(e){return a.appendChild(e).appendChild(T.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),p.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),p.getElementsByTagName=ce(function(e){return e.appendChild(T.createComment("")),!e.getElementsByTagName("*").length}),p.getElementsByClassName=J.test(T.getElementsByClassName),p.getById=ce(function(e){return a.appendChild(e).id=A,!T.getElementsByName||!T.getElementsByName(A).length}),p.getById?(x.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},x.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(x.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},x.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),x.find.TAG=p.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):p.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},x.find.CLASS=p.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(p.qsa=J.test(T.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML=" ",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+R+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+R+"*(?:value|"+I+")"),e.querySelectorAll("[id~="+A+"-]").length||v.push("~="),(t=T.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+R+"*name"+R+"*="+R+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+A+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML=" ";var t=T.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+R+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(p.matchesSelector=J.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){p.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",W)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=J.test(a.compareDocumentPosition),y=t||J.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!p.sortDetached&&t.compareDocumentPosition(e)===n?e==T||e.ownerDocument==d&&y(d,e)?-1:t==T||t.ownerDocument==d&&y(d,t)?1:u?H(u,e)-H(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==T?-1:t==T?1:i?-1:o?1:u?H(u,e)-H(u,t):0;if(i===o)return de(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?de(a[r],s[r]):a[r]==d?-1:s[r]==d?1:0}),T},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(C(e),p.matchesSelector&&E&&!k[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||p.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){k(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return Q.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&V.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+R+")"+e+"("+R+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function D(e,n,r){return b(n)?E.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?E.grep(e,function(e){return e===n!==r}):"string"!=typeof n?E.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(E.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||L,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:j.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof E?t[0]:t,E.merge(this,E.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:w,!0)),k.test(r[1])&&E.isPlainObject(t))for(r in t)b(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=w.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):b(e)?void 0!==n.ready?n.ready(e):e(E):E.makeArray(e,this)}).prototype=E.fn,L=E(w);var q=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}E.fn.extend({has:function(e){var t=E(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,pe=/^$|^module$|\/(?:java|ecma)script/i;le=w.createDocumentFragment().appendChild(w.createElement("div")),(ce=w.createElement("input")).setAttribute("type","radio"),ce.setAttribute("checked","checked"),ce.setAttribute("name","t"),le.appendChild(ce),m.checkClone=le.cloneNode(!0).cloneNode(!0).lastChild.checked,le.innerHTML="",m.noCloneChecked=!!le.cloneNode(!0).lastChild.defaultValue,le.innerHTML=" ",m.option=!!le.lastChild;var he={thead:[1,""],col:[2,""],tr:[2,""],td:[3,""],_default:[0,"",""]};function ge(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&S(e,t)?E.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n",""]);var ye=/<|?\w+;/;function me(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),d=[],p=0,h=e.length;p\s*$/g;function Le(e,t){return S(e,"table")&&S(11!==t.nodeType?t:t.firstChild,"tr")&&E(e).children("tbody")[0]||e}function je(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Oe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n",2===ft.childNodes.length),E.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(m.createHTMLDocument?((r=(t=w.implementation.createHTMLDocument("")).createElement("base")).href=w.location.href,t.head.appendChild(r)):t=w),o=!n&&[],(i=k.exec(e))?[t.createElement(i[1])]:(i=me([e],t,o),o&&o.length&&E(o).remove(),E.merge([],i.childNodes)));var r,i,o},E.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=E.css(e,"position"),c=E(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=E.css(e,"top"),u=E.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),b(t)&&(t=t.call(e,n,E.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):("number"==typeof f.top&&(f.top+="px"),"number"==typeof f.left&&(f.left+="px"),c.css(f))}},E.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){E.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===E.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===E.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=E(e).offset()).top+=E.css(e,"borderTopWidth",!0),i.left+=E.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-E.css(r,"marginTop",!0),left:t.left-i.left-E.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===E.css(e,"position"))e=e.offsetParent;return e||re})}}),E.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;E.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),E.each(["top","left"],function(e,n){E.cssHooks[n]=Fe(m.pixelPosition,function(e,t){if(t)return t=We(e,n),Ie.test(t)?E(e).position()[n]+"px":t})}),E.each({Height:"height",Width:"width"},function(a,s){E.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){E.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?E.css(e,t,i):E.style(e,t,n,i)},s,n?e:void 0,n)}})}),E.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),E.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){E.fn[n]=function(e,t){return 0=o.clientWidth&&n>=o.clientHeight}),l=0a[e]&&!t.escapeWithReference&&(n=Q(f[o],a[e]-('right'===e?f.width:f.height))),ae({},o,n)}};return l.forEach(function(e){var t=-1===['left','top'].indexOf(e)?'secondary':'primary';f=le({},f,m[t](e))}),e.offsets.popper=f,e},priority:['left','right','top','bottom'],padding:5,boundariesElement:'scrollParent'},keepTogether:{order:400,enabled:!0,fn:function(e){var t=e.offsets,o=t.popper,n=t.reference,i=e.placement.split('-')[0],r=Z,p=-1!==['top','bottom'].indexOf(i),s=p?'right':'bottom',d=p?'left':'top',a=p?'width':'height';return o[s]r(n[s])&&(e.offsets.popper[d]=r(n[s])),e}},arrow:{order:500,enabled:!0,fn:function(e,o){var n;if(!K(e.instance.modifiers,'arrow','keepTogether'))return e;var i=o.element;if('string'==typeof i){if(i=e.instance.popper.querySelector(i),!i)return e;}else if(!e.instance.popper.contains(i))return console.warn('WARNING: `arrow.element` must be child of its popper element!'),e;var r=e.placement.split('-')[0],p=e.offsets,s=p.popper,d=p.reference,a=-1!==['left','right'].indexOf(r),l=a?'height':'width',f=a?'Top':'Left',m=f.toLowerCase(),h=a?'left':'top',c=a?'bottom':'right',u=S(i)[l];d[c]-us[c]&&(e.offsets.popper[m]+=d[m]+u-s[c]),e.offsets.popper=g(e.offsets.popper);var b=d[m]+d[l]/2-u/2,w=t(e.instance.popper),y=parseFloat(w['margin'+f]),E=parseFloat(w['border'+f+'Width']),v=b-e.offsets.popper[m]-y-E;return v=ee(Q(s[l]-u,v),0),e.arrowElement=i,e.offsets.arrow=(n={},ae(n,m,$(v)),ae(n,h,''),n),e},element:'[x-arrow]'},flip:{order:600,enabled:!0,fn:function(e,t){if(W(e.instance.modifiers,'inner'))return e;if(e.flipped&&e.placement===e.originalPlacement)return e;var o=v(e.instance.popper,e.instance.reference,t.padding,t.boundariesElement,e.positionFixed),n=e.placement.split('-')[0],i=T(n),r=e.placement.split('-')[1]||'',p=[];switch(t.behavior){case ce.FLIP:p=[n,i];break;case ce.CLOCKWISE:p=G(n);break;case ce.COUNTERCLOCKWISE:p=G(n,!0);break;default:p=t.behavior;}return p.forEach(function(s,d){if(n!==s||p.length===d+1)return e;n=e.placement.split('-')[0],i=T(n);var a=e.offsets.popper,l=e.offsets.reference,f=Z,m='left'===n&&f(a.right)>f(l.left)||'right'===n&&f(a.left)f(l.top)||'bottom'===n&&f(a.top)f(o.right),g=f(a.top)f(o.bottom),b='left'===n&&h||'right'===n&&c||'top'===n&&g||'bottom'===n&&u,w=-1!==['top','bottom'].indexOf(n),y=!!t.flipVariations&&(w&&'start'===r&&h||w&&'end'===r&&c||!w&&'start'===r&&g||!w&&'end'===r&&u),E=!!t.flipVariationsByContent&&(w&&'start'===r&&c||w&&'end'===r&&h||!w&&'start'===r&&u||!w&&'end'===r&&g),v=y||E;(m||b||v)&&(e.flipped=!0,(m||b)&&(n=p[d+1]),v&&(r=z(r)),e.placement=n+(r?'-'+r:''),e.offsets.popper=le({},e.offsets.popper,C(e.instance.popper,e.offsets.reference,e.placement)),e=P(e.instance.modifiers,e,'flip'))}),e},behavior:'flip',padding:5,boundariesElement:'viewport',flipVariations:!1,flipVariationsByContent:!1},inner:{order:700,enabled:!1,fn:function(e){var t=e.placement,o=t.split('-')[0],n=e.offsets,i=n.popper,r=n.reference,p=-1!==['left','right'].indexOf(o),s=-1===['top','left'].indexOf(o);return i[p?'left':'top']=r[o]-(s?i[p?'width':'height']:0),e.placement=T(t),e.offsets.popper=g(i),e}},hide:{order:800,enabled:!0,fn:function(e){if(!K(e.instance.modifiers,'hide','preventOverflow'))return e;var t=e.offsets.reference,o=D(e.instance.modifiers,function(e){return'preventOverflow'===e.name}).boundaries;if(t.bottomo.right||t.top>o.bottom||t.rightwindow.devicePixelRatio||!fe),c='bottom'===o?'top':'bottom',g='right'===n?'left':'right',b=B('transform');if(d='bottom'==c?'HTML'===l.nodeName?-l.clientHeight+h.bottom:-f.height+h.bottom:h.top,s='right'==g?'HTML'===l.nodeName?-l.clientWidth+h.right:-f.width+h.right:h.left,a&&b)m[b]='translate3d('+s+'px, '+d+'px, 0)',m[c]=0,m[g]=0,m.willChange='transform';else{var w='bottom'==c?-1:1,y='right'==g?-1:1;m[c]=d*w,m[g]=s*y,m.willChange=c+', '+g}var E={"x-placement":e.placement};return e.attributes=le({},E,e.attributes),e.styles=le({},m,e.styles),e.arrowStyles=le({},e.offsets.arrow,e.arrowStyles),e},gpuAcceleration:!0,x:'bottom',y:'right'},applyStyle:{order:900,enabled:!0,fn:function(e){return V(e.instance.popper,e.styles),j(e.instance.popper,e.attributes),e.arrowElement&&Object.keys(e.arrowStyles).length&&V(e.arrowElement,e.arrowStyles),e},onLoad:function(e,t,o,n,i){var r=L(i,t,e,o.positionFixed),p=O(o.placement,r,t,e,o.modifiers.flip.boundariesElement,o.modifiers.flip.padding);return t.setAttribute('x-placement',p),V(t,{position:o.positionFixed?'fixed':'absolute'}),o},gpuAcceleration:void 0}}},ge});
+//# sourceMappingURL=popper.min.js.map
diff --git a/admin/static/views/auth.html b/admin/static/views/auth.html
new file mode 100644
index 00000000..0c8e3238
--- /dev/null
+++ b/admin/static/views/auth.html
@@ -0,0 +1,86 @@
+
+
+ Authentification
+
+
+
+ Générer fichtpasswd
+
+
+
+
+
+
+
+
+ OAuth 2
+ Actif
+ Non configuré
+
+
+ DexIdP
+
+
+
+
+
+
+
+
+
+ Autorité de certification
+ Générée
+ Introuvable
+
+
+
+
+
+ Aucune CA n'a été générée pour le moment.
+
+
+
+
+ {{ k }}
+ /CN={{ v.CommonName }}/OU={{ v.OrganizationalUnit }}/O={{ v.Organization }}/L={{ v.Locality }}/P={{ v.Province }}/C={{ v.Country }}/
+ {{ v }}
+
+
+
+
+
+
+
+
+
+ Association utilisateurs et équipes
+
+
+
+
+
+
+
+ Utilisateur
+
+ Équipe
+
+
+ {{ association.association }}
+ ⬌
+
+
+ {{ team.name }}
+
+
+
+
+
diff --git a/admin/static/views/claim-list.html b/admin/static/views/claim-list.html
new file mode 100644
index 00000000..140feb65
--- /dev/null
+++ b/admin/static/views/claim-list.html
@@ -0,0 +1,87 @@
+
+ Tâches et réclammations ({{ claims.length }})
+ Ajouter une tâche
+
+
+
+ Nouvelles
+
+
+ Non assignée
+
+
+ Que mes tâches
+
+
+ Tâches closes
+
+
+
+
+
+
+
+
+
+
+ {{ field }}
+
+
+
+
+
+
+
+ {{ claim[field] }}
+
+
+
+ {{ assignee.name }}
+
+
+
+
+ {{ last_update }}
+
+
+
+ {{ teams[claim.id_team].name }}
+
+
+
+
+
+
+
+
+
+
+ Assignables à
+ Ajouter une personne
+
+
+
diff --git a/admin/static/views/claim.html b/admin/static/views/claim.html
new file mode 100644
index 00000000..8b2ffdb5
--- /dev/null
+++ b/admin/static/views/claim.html
@@ -0,0 +1,75 @@
+
+ Nouveau
+ Besoin d'infos
+ Confirmer
+ En cours
+ Fait
+ Clore
+ Invalide
+
+Me l'assigner
+Tâche
+
+
diff --git a/admin/static/views/event-list.html b/admin/static/views/event-list.html
new file mode 100644
index 00000000..aa91a755
--- /dev/null
+++ b/admin/static/views/event-list.html
@@ -0,0 +1,23 @@
+
+ Événements
+ Vider la liste
+ Ajouter un événement
+
+
+
+
+
+
+
+ {{ field }}
+
+
+
+
+
+
+ {{ event[field] }}
+
+
+
+
diff --git a/admin/static/views/event.html b/admin/static/views/event.html
new file mode 100644
index 00000000..5543fedd
--- /dev/null
+++ b/admin/static/views/event.html
@@ -0,0 +1,23 @@
+
+
+
+
+
+ Save
+ Delete
+
+
+ Ajouter l'événement
+
+
diff --git a/admin/static/views/exercice-flags.html b/admin/static/views/exercice-flags.html
new file mode 100644
index 00000000..8de3f0ee
--- /dev/null
+++ b/admin/static/views/exercice-flags.html
@@ -0,0 +1,262 @@
+
+
Synchroniser
+
+
+
+
+
+
+
+
+
+
+
Dépendances :
+
+
sans
+
+
+
+
Statistiques
+
+ ID : {{ flag.id }}
+ Validés : {{ stats["completed"] }}
+
+ Tentés : {{ stats["tries"] }}
+
+
+
+ Équipes :
+ aucune
+
+
+
+
+
+
+
+
+ Ajouter choix
+
+
+ Valider pour
+
+
+
+
Aucun choix
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Dépendances :
+
+
sans
+
+
+
Statistiques
+
+ ID : {{ q.id }}
+ Validés : {{ stats["completed"] }}
+
+ Tentés : {{ stats["tries"] }}
+
+
+
+ Équipes :
+ aucune
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/static/views/exercice-list.html b/admin/static/views/exercice-list.html
new file mode 100644
index 00000000..6046a9c5
--- /dev/null
+++ b/admin/static/views/exercice-list.html
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+ Édition de masse
+
+ Les propriétés en gras seront écrasées.
+
+
+
+
diff --git a/admin/static/views/exercice-resolution.html b/admin/static/views/exercice-resolution.html
new file mode 100644
index 00000000..88d00e92
--- /dev/null
+++ b/admin/static/views/exercice-resolution.html
@@ -0,0 +1,15 @@
+
+
+
diff --git a/admin/static/views/exercice.html b/admin/static/views/exercice.html
new file mode 100644
index 00000000..dc796197
--- /dev/null
+++ b/admin/static/views/exercice.html
@@ -0,0 +1,475 @@
+
+
+
+
Différences par rapport au dépôt
+
+
+
+
+
+
+
+
+
+
+ Save
+ Delete
+
+
+ Create exercice
+
+
+
+
+
+
+
+
+
+
+
+ Points actuels
+
+
+ Défi tenté par
+
+
+ Nombre de tentatives
+
+
+ Défi validé par
+
+
+ Drapeaux validés
+ {{ stats.flag_solved.length }}
+ aucun
+
+ QCM validés
+ {{ stats.mcq_solved.length }}
+ aucun
+
+
+
+
+
+
+
+
+
+
+
+
+ Taille : {{ file.size | size }} ‐
+ BLAKE2b : {{ file.checksum | cksum }}
+
+
+
+ Publié aux équipes
+
+
+ Dépend de la validation de :
+
aucun flag
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Synchroniser
+ Ajouter
+
+
+
+
+
+
+ Fichier : {{ hint.file }}
+ Hash : {{ hint.content }}
+
+
+
+ Dépendances :
+
+
sans
+
+
+
+
+
+
+
+
+
+
+
+
+ Synchroniser
+ Ajouter
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Dépendances :
+
+
sans
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Dépendances :
+
+
sans
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.time | date:"mediumTime" }} {{ row.kind }} x{{ row.coefficient }}
+
+
+
+ {{ row.team_name }}
+
+
+ :
+ {{ row.secondary_title }}
+ {{ row.secondary_title }}
+ {{ row.secondary_title }}
+
+ : {{ row.secondary }}
+ , {{ line.kind }}#{{ line.related }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Coefficient
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/static/views/exercices-forgelink.html b/admin/static/views/exercices-forgelink.html
new file mode 100644
index 00000000..2588ebc4
--- /dev/null
+++ b/admin/static/views/exercices-forgelink.html
@@ -0,0 +1,12 @@
+Accès rapide aux exercices
+
+
diff --git a/admin/static/views/file-list.html b/admin/static/views/file-list.html
new file mode 100644
index 00000000..83a3242c
--- /dev/null
+++ b/admin/static/views/file-list.html
@@ -0,0 +1,43 @@
+
+ Fichiers
+
+
+ Vérifier les fichiers
+ Gunzip
+ Nuke files
+
+
+
+
+
+
+
+
+ {{ field }}
+
+ checksum
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ file[field] }}
+
+
+
+ {{ file.checksum | bto16 }}
+ {{ file.checksum_shown | bto16 }}
+
+
+
+
diff --git a/admin/static/views/home.html b/admin/static/views/home.html
index 1a196897..720f46af 100644
--- a/admin/static/views/home.html
+++ b/admin/static/views/home.html
@@ -1,9 +1,111 @@
-
-
Interface d'administration du challenge
-
- Sélectionnez une action dans le menu ci-dessus.
-
-
- Version de l'API : {{ v.version }}
-
+
+
Interface d'administration du challenge
+
+
+
+ Version de l'API : {{ v.version }}
+
+
+ Latence frontal :
+ Dernière synchronisation du frontal : {{ t.frontend | date:"mediumTime" }}
+
+
+
+
+
+
+ Problèmes dans les fichiers :
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ team.rank }}
+ {{ team.name }}
+ {{ team.score | number:0 }}
+
+
+
+
+
diff --git a/admin/static/views/pki.html b/admin/static/views/pki.html
new file mode 100644
index 00000000..30f22686
--- /dev/null
+++ b/admin/static/views/pki.html
@@ -0,0 +1,158 @@
+
+ Certificats clients
+ Générer un certificat
+
+ Générer fichtpasswd
+
+
+
+
+
+
+
+
+ Serial
+ Date de création
+ Équipe
+ Révoqué ?
+ Action
+
+
+
+
+ {{ certificate.id }}
+ {{ certificate.creation }}
+
+
+ {{ team.name }}
+
+
+
+ Associer
+
+ {{ certificate.revoked }}
+
+ Télécharger
+ Révoquer
+
+
+
+
+
+
+
+
+
+
+
+ Êtes-vous sûr de vouloir révoquer le certificat ?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Autorité de certification
+ Générée
+ Introuvable
+ Générer
+ Regénérer
+
+
+
+
Aucune CA n'a été générée pour le moment.
+
+
+
+
+
+
+
+ {{ k }}
+ /CN={{ v.CommonName }}/OU={{ v.OrganizationalUnit }}/O={{ v.Organization }}/L={{ v.Locality }}/P={{ v.Province }}/C={{ v.Country }}/
+ {{ v }}
+
+
diff --git a/admin/static/views/public.html b/admin/static/views/public.html
new file mode 100644
index 00000000..ab4e5dac
--- /dev/null
+++ b/admin/static/views/public.html
@@ -0,0 +1,407 @@
+
+
+ Interface publique
+
+
+
+ Scène prédéfinie
+
+
+
+ élm. côté
+ élm. central
+
+ Publier cette scène
+
+ Toggle Dropdown
+
+
+
+
+
+
+
+
+ Aucun contenu n'est actuellement affiché.
+ Aucun contenu à afficher.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Cacher les événements
+
+
+
+
+
+
+ Cacher le timer
+
+
+
+
+
+
+ Cacher le carousel
+
+
+
+
+
+
diff --git a/admin/static/views/repositories.html b/admin/static/views/repositories.html
new file mode 100644
index 00000000..daca48fa
--- /dev/null
+++ b/admin/static/views/repositories.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+ Chemin
+ Branche
+ Commit Plus récent
+
+
+
+
+ {{ repository.path }}
+ {{ repository.branch }}
+
+ {{ repository.hash }}
+
+
+
+
+
+
+
+
+
diff --git a/admin/static/views/settings.html b/admin/static/views/settings.html
new file mode 100644
index 00000000..203f2b2f
--- /dev/null
+++ b/admin/static/views/settings.html
@@ -0,0 +1,531 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ m.association }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Changements anticipés
+
+
+
+
+ {{ k }} → {{ v }}
+
+
+
+
+
+
+
+
+
+
+
+ Je sais ce que le challenge a démarré ET j'ai réalisé une sauvegarde de la base de données il y a moins d'une minute ET je sais que c'est une très mauvaise idée de cocher cette case , mais j'y suis obligé pour de bonnes raisons.
+
+
+
+
+
+
+
+
Paramètres de synchronisation
+
Revenir aux paramètres par défaut
+
Effacer les challenges et les thèmes
+
Effacer les équipes
+
Effacer la partie (tentatives, indices, ...)
+
Effacer les annexes (events, issues, QA)
+
+
+
+
+
diff --git a/admin/static/views/sync.html b/admin/static/views/sync.html
new file mode 100644
index 00000000..06d8e84f
--- /dev/null
+++ b/admin/static/views/sync.html
@@ -0,0 +1,159 @@
+
+
+
+
+
+
+ Je sais ce que le challenge a démarré ET j'ai réalisé une sauvegarde de la base de données il y a moins d'une minute ET je sais que c'est une très mauvaise idée de cocher cette case , mais j'y suis obligé pour de bonnes raisons.
+
+
+
+
+
+
+
+
+
+
+ Type
+
+ Synchronisation
+
+ ID
+
+ {{ syncStatus['sync-id'] }}
+ Pull
+
+ Statut
+
+ {{ syncPercent }} %
+ Pull
+ Synchronisation
+
+
+
+
{{ syncStatus.lastError }}
+
+
+
+ Synchronisation intégrale
+
+ Toggle Dropdown
+
+
+
+
Synchronisation sans fichiers
+
Mettre à disposition les vidéos
+
Effacer les solutions
+
+
+
+
+
+
+
+
+
+
Lancez la génération du rapport pour lister les différences.
+
+
+
diff --git a/admin/static/views/tags.html b/admin/static/views/tags.html
new file mode 100644
index 00000000..a24dbf27
--- /dev/null
+++ b/admin/static/views/tags.html
@@ -0,0 +1,34 @@
+
+
Tags
+
+
+
+
+
+
+ Tag
+
+
+ Nb
+
+
+ Exercices
+
+
+
+
+
+
+ {{ tag }}
+
+
+ {{ exercices.length }}
+
+
+
+ {{ e.title }}
+
+
+
+
+
diff --git a/admin/static/views/team-edit.html b/admin/static/views/team-edit.html
new file mode 100644
index 00000000..9d215896
--- /dev/null
+++ b/admin/static/views/team-edit.html
@@ -0,0 +1,224 @@
+
+ {{ team.name }}
+
+
+ Score
+
+
+
+ Statistiques
+
+
+
+
+
+
+
+
+
Identifiant
+
+
+
+
+
+
+
+ Équipe active (inclure lors des générations)
+
+
+
+
+
+
+
Mot de passe
+
+
+
+
+
+
+
+
+ Save
+ Delete
+
+
+ Create team
+
+
+
+
+
+
+
+
+
+ This team has no member!
+
+
+
+
{{ field | capitalize }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Numéro de série
+ Dissocier
+
+
+ {{ cert.id }}
+ Révoqué
+
+
+ Date de création
+ Télécharger
+
+ {{ cert.creation }}
+ Mot de passe
+ {{ cert.password }}
+ Date de révocation
+ {{ cert.revoked }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/static/views/team-export.html b/admin/static/views/team-export.html
new file mode 100644
index 00000000..713c448c
--- /dev/null
+++ b/admin/static/views/team-export.html
@@ -0,0 +1,22 @@
+
+
+
+
{{ team.name }} {{ teams[my.team_id].rank }}/{{ nb_teams }} –
+
+
+
+
+
+
+ {{ exercice.title }}
+ ({{ my.exercices[eid].solved_rank }}e – – {{ my.exercices[eid].hints | countHints }} indices )
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/static/views/team-list.html b/admin/static/views/team-list.html
index 64526d21..5b175d2f 100644
--- a/admin/static/views/team-list.html
+++ b/admin/static/views/team-list.html
@@ -1,17 +1,40 @@
-
-
-
+
+
+ Équipes
+
+
+ Ajouter une équipe
+
+
+ Import Cyberrange
+
+
+
+ Statistiques générales
+ Désactiver les équipes inactives
+
+
+
+
+
+
{{ field }}
+
+ color
+
-
+
{{ team[field] }}
+
+ {{ team['color'] | toColor }}
+
diff --git a/admin/static/views/team-new.html b/admin/static/views/team-new.html
deleted file mode 100644
index 8f1f14ee..00000000
--- a/admin/static/views/team-new.html
+++ /dev/null
@@ -1,11 +0,0 @@
-New team
-
-
-
-
-
-
diff --git a/admin/static/views/team-print.html b/admin/static/views/team-print.html
new file mode 100644
index 00000000..c082db2e
--- /dev/null
+++ b/admin/static/views/team-print.html
@@ -0,0 +1,38 @@
+{{ config.title }} – Équipes
+
+
+
+
+
+
+
+ ID
+
+
+ Nom d'équipe
+
+
+ members
+
+
+
+
+
+
+ {{ team.id }}
+
+
+ {{ team.name }}
+
+
+
+
+
+ {{ member[field] }}
+
+
+
+
+
+
+
diff --git a/admin/static/views/team-score.html b/admin/static/views/team-score.html
new file mode 100644
index 00000000..e0f34b5c
--- /dev/null
+++ b/admin/static/views/team-score.html
@@ -0,0 +1,42 @@
+
+
+
+
+
+ Exercice
+ Raison
+ Points
+ Calcul
+ Date
+
+
+
+
+ {{ exercice.title }}
+
+
+ {{ row.reason }}
+
+
+ {{ row.points * row.coeff }}
+
+
+ {{ row.points }} * {{ row.coeff }}
+
+
+ {{ row.points }} * {{ settings.questionGainRatio }} / {{ settings.questionGainRatio / row.coeff }}
+
+
+ {{ row.time | date:"mediumTime" }}
+
+
+
+
+
+ {{ my.score100 / 100 }}
+
+
+
diff --git a/admin/static/views/team-stats.html b/admin/static/views/team-stats.html
new file mode 100644
index 00000000..4bb75eb1
--- /dev/null
+++ b/admin/static/views/team-stats.html
@@ -0,0 +1,57 @@
+
+
+
+ {{ team.name }}
+
+ et , {{ member.firstname | capitalize }} {{ member.nickname }} {{ member.lastname | capitalize }}
+
+
+
+
+
+
+ Points
+ {{ my.score }}
+ Classement
+ {{ teams[my.team_id].rank }}/{{ nb_teams }} ({{ nb_reg_teams }} registered teams)
+
+
+
Présence
+
+
+
+
+
Exercices résolus : {{ solved_exercices }}/{{ exercices.length }} {{ solved_exercices * 100 / exercices.length | number:0 }}%
+
+
+
+
+
+
+
+
+
+
Tentatives par niveaux
+
+
+
Tentatives par thèmes
+
+
+
+
diff --git a/admin/static/views/team.html b/admin/static/views/team.html
deleted file mode 100644
index 62efad25..00000000
--- a/admin/static/views/team.html
+++ /dev/null
@@ -1,54 +0,0 @@
-
-
-{{ team.name }} ({{ team.initialName}}) et , {{ member.firstname | capitalize }} {{ member.nickname }} {{ member.lastname | capitalize }}
-
-
-
-
- Points
- {{ my.score }}
- Classement
- {{ teams[my.team_id].rank }}/{{ nb_teams }} ({{ nb_reg_teams }} registered teams)
-
-
-
Présence
-
-
-
-
-
Exercices résolus : {{ solved_exercices }}/{{ exercices.length }} {{ solved_exercices * 100 / exercices.length | number:0 }}%
-
-
-
-
{{ theme.name }}
-
-
-
-
-
-
-
-
-
-
-
-
Tentatives par niveaux
-
-
-
Tentatives par thèmes
-
-
-
-
-
diff --git a/admin/static/views/theme-list.html b/admin/static/views/theme-list.html
index 2e5b4863..c5a292f7 100644
--- a/admin/static/views/theme-list.html
+++ b/admin/static/views/theme-list.html
@@ -1,6 +1,13 @@
-
-
+
+
+
+
+ Veuillez activer le JavaScript. Ce site requiert un navigateur interprêtant le JavaScript pour fonctionner. Veuillez l'activer ou en télécharger un supportant cette technologie.
+
+
+
+
+
+
+
+
Bienvenue au {{ challenge.title }} !
+
+ Avant de vous installer, venez récupérer votre clef USB auprès
+ de notre équipe. Elle contient le certificat qui vous permettra
+ de vous authentifier auprès de notre serveur.
+
+
+ Une fois connecté au réseau, contactez notre serveur sur :
+
+
+
+
+
+
+
+
+
+
+
{{ duration / 60 | time }} : {{ duration % 60 | time }}
+
{{ s.params.lead }}
+
+
+
+
+
+
Bienvenue au {{ challenge.title }} !
+
+ Durant ce challenge, les équipes doivent remonter des scénarii
+ d'attaques auxquels nos systèmes d'information font face
+ chaque jour : fuite de données, compromission d'un poste de
+ travail, exploitation de vulnérabilités d'un site web, ...
+
+
+ Pour valider un challenge, chaque participant va télécharger :
+ soit des journaux d'évènements , des extraits de trafic réseau
+ ou même des copies figées de la mémoire vive de machines
+ malveillantes, pour essayer de comprendre comment l'attaquant a
+ contourné la sécurité de la machine et quelles actions hostiles
+ ont été effectuées.
+
+
+
+
+
+
+
+
+
+
+
+ Challenge {{ themes[my.exercices[s.params.exercice].theme_id].exercices[s.params.exercice].title }} du thème {{ themes[my.exercices[s.params.exercice].theme_id].name }}
+ par {{ themes[my.exercices[s.params.exercice].theme_id].authors | stripHTML }}
+
+
+
+ Rapporte
+
+
+ Tenté par
+
+ Résolu par
+
+
+
+
+
+
+
+
+
+ {{ th.name }}
+
+
+
+
+ Challenge {{ lvl }}
+
+
+ {{ exercice.solved }}
+ {{ exercice.tried }}
+
+
+
+
+
+
+ {{ team.rank }}er e — {{ team.score | number:0 }} points{{ team.name }}
+
+ {{ mystats.themes[tid].solved }}/{{ mystats.themes[tid].total }}
+ ({{ mystats.themes[tid].tries }})
+
+
+
+
+
+
+ Résolus
+ Total résolus
+ Tentatives
+
+
+ {{ mystats.themes[tid].solved }}
+ {{ mystats.themes[tid].tries }}
+
+
+
+
+
+
+
+
+
+
+ Place
+ Équipe
+ Score
+
+
+
+
+ {{ r.rank }}er e
+ {{ r.name }}
+ {{ r.score | number:0 }}
+
+
+
+
+ {{ rank[0].rank }}er
+ {{ rank[0].name }}
+ {{ rank[0].score | number:0 }}
+
+
+
+
+ {{ member.firstname }} {{ member.lastname | capitalize }} ({{ member.company }})
+
+
+
+
+
+ {{ rank[1].rank }}e
+ {{ rank[1].name }}
+ {{ rank[1].score | number:0 }}
+
+
+
+
+ {{ member.firstname }} {{ member.lastname | capitalize }} ({{ member.company }})
+
+
+
+
+
+ {{ rank[2].rank }}e
+ {{ rank[2].name }}
+ {{ rank[2].score | number:0 }}
+
+
+
+
+ {{ member.firstname }} {{ member.lastname | capitalize }} ({{ member.company }})
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Classement : {{ team.rank }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Place
+ Équipe
+ Score
+
+
+
+
+ {{ r.rank }}er e
+ {{ r.name }}
+ {{ r.score | number:0 }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Challenge {{ themes[exercice.theme_id].exercices[eid].title }} du thème {{ themes[exercice.theme_id].name }}
+
+ par {{ themes[exercice.theme_id].authors }}
+
+
+
+ Rapporte
+
+
+ Tenté par
+
+ Résolu par
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Le {{ challenge.title }} !
+
+ Durant ce challenge, les équipes doivent remonter des scénarii
+ d'attaques auxquels nos systèmes d'information font face
+ chaque jour : fuite de données, compromission d'un poste de
+ travail, exploitation de vulnérabilités d'un site web, ...
+
+
+ Pour valider un challenge, chaque participant va télécharger :
+ soit des journaux d'évènements , des extraits de trafic réseau
+ ou même des copies figées de la mémoire vive de machines
+ malveillantes, pour essayer de comprendre comment l'attaquant a
+ contourné la sécurité de la machine et quelles actions hostiles
+ ont été effectuées.
+
+
+
+
+
+
+
+ Les entreprises ciblées
+
+
+
+
+
+
+
+
+
+
Défi {{ exercices[s.params.exercice].title }} du thème {{ themes[my.exercices[s.params.exercice].theme_id].name }}
+
+
+
+
+
+ Tenté par
+ Résolu par
+
+
+
+
+
+
+
+ Challenges à la une
+
+
+
+
+
+
{{ exercices[lastExercice].title }} du thème {{ themes[my.exercices[lastExercice].theme_id].name }}
+
+
+
+
+
+ Tenté par
+ Résolu par
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Place
+ Équipe
+ Score
+
+
+
+
+ {{ r.rank }}er e
+ {{ r.name }}
+ {{ r.score | number:0 }}
+
+
+
+
+
+
+
+
+
+
+
+ Place
+ Équipe
+ Score
+
+
+
+
+ {{ r.rank }}er e
+ {{ r.name }}
+ {{ r.score | number:0 }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ duration / 60 | time }} : {{ duration % 60 | time }}
+
00 : 00
+
+
+
+
+
+
+
+
+
+
+ {{ time.hours | time }}
+ :
+ {{ time.minutes | time }}
+ :
+ {{ time.seconds | time }}
+
+
+ Temps restant du challenge forensic
+ Le challenge forensic va bientôt commencer !
+ Le challenge forensic est terminé !
+
+
+
+ {{ time.start | date:"shortDate" }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Bienvenue au {{ challenge.title }} !
+
+
+
+
+
+ Ce challenge met en scène des scénarii d'attaques auxquels
+ nos systèmes d'information font face chaque jour.
+
+
+
+
+
+
+ Les {{ teams | objectLength }} équipes doivent, en 1 journée, retracer les attaques à la
+ recherche des données confidentielles exfiltrées.
+
+
+
+
+
+
+
+
+
+ Les challenges ont été réalisés par les étudiants de la
+ spécialisation SRS de l'Épita.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ settings.start - 1800000 | date:"HH' h 'mm" }}
+ Accueil des équipes
+
+
+ {{ settings.start | date:"HH' h 'mm" }}
+ Début du challenge
+
+
+ {{ settings.end | date:"HH' h 'mm" }}
+ Fin du challenge
+
+
+ demain {{ settings.awards | date:"dd MMM" }} {{ settings.awards | date:"HH' h 'mm" }}
+ Remise des prix
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |" "${PATH_STATIC}/index.html"
+
+exit 0
diff --git a/configs/nginx/auth/client-cert.conf b/configs/nginx/auth/client-cert.conf
new file mode 100644
index 00000000..7fcbb0c7
--- /dev/null
+++ b/configs/nginx/auth/client-cert.conf
@@ -0,0 +1,3 @@
+ssl_client_certificate /srv/PKI/shared/ca.pem;
+ssl_trusted_certificate /srv/PKI/shared/ca.pem;
+ssl_verify_client optional;
diff --git a/configs/nginx/auth/none.conf b/configs/nginx/auth/none.conf
new file mode 100644
index 00000000..e69de29b
diff --git a/configs/nginx/auth/oidc.conf b/configs/nginx/auth/oidc.conf
new file mode 100644
index 00000000..8e629385
--- /dev/null
+++ b/configs/nginx/auth/oidc.conf
@@ -0,0 +1,50 @@
+error_page 401 = @error401;
+
+location /challenge_access {
+ # forward the /validate request to Vouch Proxy
+ proxy_pass http://auth:9090;
+ # be sure to pass the original host header
+ proxy_set_header Host $http_host;
+
+ # Vouch Proxy only acts on the request headers
+ proxy_pass_request_body off;
+ proxy_set_header Content-Length "";
+
+ # optionally add X-Vouch-User as returned by Vouch Proxy along with the request
+ auth_request_set $auth_resp_x_vouch_user $upstream_http_x_vouch_user;
+
+ # these return values are used by the @error401 call
+ auth_request_set $auth_resp_jwt $upstream_http_x_vouch_jwt;
+ auth_request_set $auth_resp_err $upstream_http_x_vouch_err;
+ auth_request_set $auth_resp_failcount $upstream_http_x_vouch_failcount;
+}
+
+# If the user is not logged in, redirect them to Vouch's login URL
+location @error401 {
+ return 302 https://live.fic.srs.epita.fr/challenge_access/login?url=https://live.fic.srs.epita.fr$request_uri&vouch-failcount=$auth_resp_failcount&X-Vouch-Token=$auth_resp_jwt&error=$auth_resp_err;
+}
+
+location /auth {
+ proxy_pass http://auth:5556;
+}
+location /approval {
+ proxy_pass http://auth:5556;
+}
+location /token {
+ proxy_pass http://auth:5556;
+}
+location /keys {
+ proxy_pass http://auth:5556;
+}
+location /userinfo {
+ proxy_pass http://auth:5556;
+}
+location /static {
+ proxy_pass http://auth:5556;
+}
+location /theme {
+ proxy_pass http://auth:5556;
+}
+location /.well-known/openid-configuration {
+ proxy_pass http://auth:5556;
+}
diff --git a/configs/nginx/base/demo.conf b/configs/nginx/base/demo.conf
new file mode 100644
index 00000000..1c607ede
--- /dev/null
+++ b/configs/nginx/base/demo.conf
@@ -0,0 +1,245 @@
+proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=STATIC:10m inactive=24h max_size=1g;
+proxy_connect_timeout 1s;
+
+server_tokens off;
+
+server {
+ listen 80 default;
+
+ rewrite ^ https://$host$request_uri permanent;
+}
+
+server {
+ listen 443 default ssl http2;
+
+ ssl_protocols TLSv1.2 TLSv1.3;
+ ssl_dhparam /etc/nginx/ssl/dhparams-4096.pem;
+ ssl_prefer_server_ciphers on;
+
+ ssl_certificate /etc/nginx/ssl/fullchain.pem;
+ ssl_certificate_key /etc/nginx/ssl/privkey.pem;
+
+ ssl_client_certificate /srv/PKI/shared/ca.pem;
+ ssl_trusted_certificate /srv/PKI/shared/ca.pem;
+ ssl_verify_client optional;
+
+ root /srv/htdocs-frontend/;
+
+ error_page 401 /welcome.html;
+ error_page 403 404 /e404.html;
+ error_page 413 /e413.html;
+ error_page 500 502 504 /e500.html;
+
+ add_header Strict-Transport-Security max-age=31536000;
+ add_header X-Frame-Options deny;
+ add_header Content-Security-Policy "script-src 'unsafe-inline' 'self' 'unsafe-eval'; img-src 'self' data:; style-src 'unsafe-inline' 'self'; font-src 'self'; default-src 'self'";
+ add_header X-Xss-Protection "1; mode=block";
+ add_header X-Content-Type-Options nosniff;
+ add_header Referrer-Policy strict-origin;
+ add_header Feature-Policy "accelerometer 'none'; ambient-light-sensor 'none'; autoplay 'none'; battery 'none'; camera 'none'; display-capture 'none'; document-domain 'none'; encrypted-media 'none'; fullscreen 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; midi 'none'; payment 'none'; picture-in-picture 'none'; speaker 'none'; sync-xhr 'none'; usb 'none'; vr 'none'; wake-lock 'none'; xr-spatial-tracking 'none'";
+
+ location = / {
+ include fic-auth.conf;
+ }
+ location = /auth {
+ internal;
+ proxy_pass https://163.5.55.58/remote.php/webdav/;
+ proxy_pass_request_body off;
+ proxy_set_header Host "owncloud.srs.epita.fr";
+ proxy_set_header Content-Length "";
+ proxy_set_header X-Original-URI $request_uri;
+ }
+ location = /index.html {
+ include fic-auth.conf;
+ }
+ location = /welcome.html {
+ internal;
+ if ($http_accept ~ "^application/json") {
+ rewrite ^/(.*).html$ /$1.json;
+ }
+ }
+ location = /e404.html {
+ internal;
+ if ($http_accept ~ "^application/json") {
+ rewrite ^/(.*).html$ /$1.json;
+ }
+ }
+ location = /e413.html {
+ internal;
+ if ($http_accept ~ "^application/json") {
+ rewrite ^/(.*).html$ /$1.json;
+ }
+ }
+ location = /e500.html {
+ internal;
+ if ($http_accept ~ "^application/json") {
+ rewrite ^/(.*).html$ /$1.json;
+ }
+ }
+
+ location ~ ^/([A-Z]|_/) {
+ include fic-auth.conf;
+
+ rewrite ^/.*$ /index.html;
+ }
+
+ location /edit {
+ include fic-auth.conf;
+
+ rewrite ^/.*$ /index.html;
+ }
+ location /issues {
+ include fic-auth.conf;
+
+ rewrite ^/.*$ /index.html;
+ }
+ location /rank {
+ include fic-auth.conf;
+
+ rewrite ^/.*$ /index.html;
+ }
+ location /tags/ {
+ include fic-auth.conf;
+
+ rewrite ^/.*$ /index.html;
+ }
+ location /register {
+ include fic-auth.conf;
+
+ rewrite ^/.*$ /index.html;
+ }
+ location /rules {
+ include fic-auth.conf;
+
+ rewrite ^/.*$ /index.html;
+ }
+
+ location /files/ {
+ alias /srv/FILES/;
+ sendfile on;
+ tcp_nodelay on;
+ gzip_static always;
+ }
+
+ location /wait.json {
+ include fic-auth.conf;
+
+ root /srv/TEAMS/$team/;
+ expires epoch;
+ add_header Cache-Control no-cache;
+ }
+ location /stats.json {
+ root /srv/TEAMS/;
+ expires epoch;
+ add_header Cache-Control no-cache;
+ }
+ location /my.json {
+ include fic-auth.conf;
+
+ root /srv/TEAMS/$team/;
+ expires epoch;
+ add_header Cache-Control no-cache;
+
+ if (!-f /srv/startingblock/started) {
+ rewrite ^/.* /wait.json;
+ }
+ }
+ location /issues.json {
+ include fic-auth.conf;
+
+ root /srv/TEAMS/$team/;
+ expires epoch;
+ add_header Cache-Control no-cache;
+ }
+ location /scores.json {
+ include fic-auth.conf;
+
+ root /srv/TEAMS/$team/;
+ expires epoch;
+ add_header Cache-Control no-cache;
+ }
+ location = /events.json {
+ root /srv/TEAMS/;
+ expires epoch;
+ add_header Cache-Control no-cache;
+ }
+ location = /teams.json {
+ root /srv/TEAMS/;
+ expires epoch;
+ add_header Cache-Control no-cache;
+ }
+ location = /themes.json {
+ root /srv/TEAMS/;
+ expires epoch;
+ add_header Cache-Control no-cache;
+ }
+ location = /challenge.json {
+ root /srv/SETTINGSDIST/;
+ expires epoch;
+ add_header X-FIC-time $msec;
+ add_header Cache-Control no-cache;
+ }
+ location = /settings.json {
+ root /srv/SETTINGSDIST/;
+ expires epoch;
+ add_header X-FIC-time $msec;
+ add_header Cache-Control no-cache;
+ }
+
+ location /submit/ {
+ include fic-auth.conf;
+
+ proxy_pass http://receiver:8080/submission;
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header X-FIC-Team $team;
+ proxy_redirect off;
+ }
+ location /issue {
+ include fic-auth.conf;
+
+ proxy_pass http://receiver:8080;
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header X-FIC-Team $team;
+ proxy_redirect off;
+ }
+ location /chname {
+ include fic-auth.conf;
+
+ proxy_pass http://receiver:8080;
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header X-FIC-Team $team;
+ proxy_redirect off;
+ }
+ location /registration {
+ include fic-auth.conf;
+
+ proxy_pass http://receiver:8080;
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header X-FIC-Team $team;
+ proxy_redirect off;
+ }
+ location /reset_progress {
+ include fic-auth.conf;
+
+ proxy_pass http://receiver:8080;
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header X-FIC-Team $team;
+ proxy_redirect off;
+ }
+ location /openhint/ {
+ include fic-auth.conf;
+
+ proxy_pass http://receiver:8080;
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header X-FIC-Team $team;
+ proxy_redirect off;
+ }
+ location /wantchoices/ {
+ include fic-auth.conf;
+
+ proxy_pass http://receiver:8080;
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header X-FIC-Team $team;
+ proxy_redirect off;
+ }
+}
diff --git a/configs/nginx/base/docker.conf b/configs/nginx/base/docker.conf
new file mode 100644
index 00000000..63d29b78
--- /dev/null
+++ b/configs/nginx/base/docker.conf
@@ -0,0 +1,282 @@
+server_tokens off;
+proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=STATIC:10m inactive=24h max_size=1g;
+proxy_connect_timeout 1s;
+
+server {
+ listen 80 default;
+ listen [::]:80 default;
+
+ include fic-auth.conf;
+
+ root ${PATH_STATIC};
+
+ error_page 401 /welcome.html;
+ error_page 403 404 /e404.html;
+ error_page 413 404 /e413.html;
+ error_page 500 502 504 /e500.html;
+
+ error_page 401 /welcome.html;
+ error_page 403 404 /e404.html;
+ error_page 413 /e413.html;
+ error_page 500 502 504 /e500.html;
+
+ add_header Strict-Transport-Security max-age=31536000;
+ add_header X-Frame-Options deny;
+ add_header Content-Security-Policy "script-src 'unsafe-inline' 'self' 'unsafe-eval'; img-src 'self' data:; style-src 'unsafe-inline' 'self'; font-src 'self'; default-src 'self'";
+ add_header X-Xss-Protection "1; mode=block";
+ add_header X-Content-Type-Options nosniff;
+ add_header Referrer-Policy strict-origin;
+ add_header Feature-Policy "accelerometer 'none'; ambient-light-sensor 'none'; autoplay 'none'; battery 'none'; camera 'none'; display-capture 'none'; document-domain 'none'; encrypted-media 'none'; fullscreen 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; midi 'none'; payment 'none'; picture-in-picture 'none'; speaker 'none'; sync-xhr 'none'; usb 'none'; vr 'none'; wake-lock 'none'; xr-spatial-tracking 'none'";
+
+ location = / {
+ include fic-get-team.conf;
+ }
+ location = /index.html {
+ include fic-get-team.conf;
+ }
+ location = /welcome.html {
+ internal;
+ if ($http_accept ~ "^application/json") {
+ rewrite ^/(.*).html$ /$1.json;
+ }
+ }
+ location = /e404.html {
+ internal;
+ if ($http_accept ~ "^application/json") {
+ rewrite ^/(.*).html$ /$1.json;
+ }
+ }
+ location = /e413.html {
+ internal;
+ if ($http_accept ~ "^application/json") {
+ rewrite ^/(.*).html$ /$1.json;
+ }
+ }
+ location = /e500.html {
+ internal;
+ if ($http_accept ~ "^application/json") {
+ rewrite ^/(.*).html$ /$1.json;
+ }
+ }
+
+ location ${FIC_BASEURL2} {
+ rewrite ^${FIC_BASEURL2}(.*)$ /$1;
+ }
+
+ location ~ ^/([A-Z]|_/) {
+ include fic-get-team.conf;
+
+ rewrite ^/.*$ /index.html;
+ }
+
+ location /edit {
+ include fic-get-team.conf;
+
+ rewrite ^/.*$ /index.html;
+ }
+ location /issues {
+ include fic-get-team.conf;
+
+ rewrite ^/.*$ /index.html;
+ }
+ location /rank {
+ include fic-get-team.conf;
+
+ rewrite ^/.*$ /index.html;
+ }
+ location /tags/ {
+ include fic-get-team.conf;
+
+ rewrite ^/.*$ /index.html;
+ }
+ location /register {
+ include fic-get-team.conf;
+
+ rewrite ^/.*$ /index.html;
+ }
+ location /rules {
+ include fic-get-team.conf;
+
+ rewrite ^/.*$ /index.html;
+ }
+
+ location /files/ {
+ alias ${PATH_FILES}/;
+ sendfile on;
+ tcp_nodelay on;
+ gzip_static always;
+ }
+
+ location /wait.json {
+ include fic-get-team.conf;
+
+ root ${PATH_TEAMS}/$team/;
+ expires epoch;
+ add_header Cache-Control no-cache;
+ }
+ location /stats.json {
+ root ${PATH_TEAMS}/;
+ expires epoch;
+ add_header Cache-Control no-cache;
+ }
+ location /my.json {
+ include fic-get-team.conf;
+
+ root ${PATH_TEAMS}/$team/;
+ expires epoch;
+ add_header Cache-Control no-cache;
+
+ if (!-f ${PATH_STARTINGBLOCK}/started) {
+ rewrite ^/ /wait.json;
+ }
+ }
+ location /issues.json {
+ include fic-get-team.conf;
+
+ root ${PATH_TEAMS}/$team/;
+ expires epoch;
+ add_header Cache-Control no-cache;
+ }
+ location /scores.json {
+ include fic-get-team.conf;
+
+ root ${PATH_TEAMS}/$team/;
+ expires epoch;
+ add_header Cache-Control no-cache;
+ }
+ location /teams.json {
+ root ${PATH_TEAMS};
+ expires epoch;
+ add_header Cache-Control no-cache;
+ }
+ location /themes.json {
+ include fic-get-team.conf;
+
+ root ${PATH_TEAMS};
+ expires epoch;
+ add_header Cache-Control no-cache;
+
+ if (!-f ${PATH_TEAMS}/$team/my.json) {
+ rewrite ^/ /themes-wait.json break;
+ }
+ }
+ location /challenge.json {
+ root ${PATH_SETTINGS}/;
+ expires epoch;
+ add_header X-FIC-time $msec;
+ add_header Cache-Control no-cache;
+ }
+ location /settings.json {
+ root ${PATH_SETTINGS}/;
+ expires epoch;
+ add_header X-FIC-time $msec;
+ add_header Cache-Control no-cache;
+ }
+
+ location /submit/ {
+ include fic-get-team.conf;
+
+ proxy_pass http://${HOST_RECEIVER}/submission/;
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header X-FIC-Team $team;
+ proxy_redirect off;
+ }
+ location /issue {
+ include fic-get-team.conf;
+
+ proxy_pass http://${HOST_RECEIVER};
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header X-FIC-Team $team;
+ proxy_redirect off;
+ }
+ location /chname {
+ include fic-get-team.conf;
+
+ proxy_pass http://${HOST_RECEIVER};
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header X-FIC-Team $team;
+ proxy_redirect off;
+ }
+ location /registration {
+ include fic-get-team.conf;
+
+ proxy_pass http://${HOST_RECEIVER};
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header X-FIC-Team $team;
+ proxy_redirect off;
+ }
+ location /reset_progress {
+ include fic-get-team.conf;
+
+ proxy_pass http://${HOST_RECEIVER};
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header X-FIC-Team $team;
+ proxy_redirect off;
+ }
+ location /openhint/ {
+ include fic-get-team.conf;
+
+ proxy_pass http://${HOST_RECEIVER};
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header X-FIC-Team $team;
+ proxy_redirect off;
+ }
+ location /wantchoices/ {
+ include fic-get-team.conf;
+
+ proxy_pass http://${HOST_RECEIVER};
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header X-FIC-Team $team;
+ proxy_redirect off;
+ }
+
+ location /api {
+ include fic-get-team.conf;
+
+ proxy_pass http://${HOST_ADMIN}${FIC_BASEURL}admin/api;
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header X-FIC-Team $team;
+ proxy_redirect off;
+ }
+
+ location ${FIC_BASEURL}admin {
+ proxy_pass http://${HOST_ADMIN};
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_redirect off;
+ }
+
+ location ${FIC_BASEURL}admin/api {
+ proxy_pass http://${HOST_ADMIN};
+ proxy_read_timeout 400s;
+ proxy_set_header X-Forwarded-For $remote_addr;
+ }
+
+ location ${FIC_BASEURL}qa {
+ include fic-get-team.conf;
+
+ proxy_pass http://${HOST_QA};
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header X-FIC-Team $team;
+ proxy_redirect off;
+ }
+
+ location ${FIC_BASEURL}dashboard {
+ include fic-get-team.conf;
+
+ proxy_pass http://${HOST_DASHBOARD};
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header X-FIC-Team $team;
+ proxy_redirect off;
+ }
+
+ location = /events.json {
+ proxy_pass http://${HOST_ADMIN}/api/events/;
+ proxy_method GET;
+ proxy_pass_request_body off;
+ proxy_set_header Content-Length "";
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_redirect off;
+ proxy_cache STATIC;
+ proxy_cache_valid 3s;
+ }
+}
diff --git a/configs/nginx/base/prod.conf b/configs/nginx/base/prod.conf
new file mode 100644
index 00000000..07d7d27e
--- /dev/null
+++ b/configs/nginx/base/prod.conf
@@ -0,0 +1,237 @@
+proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=STATIC:10m inactive=24h max_size=1g;
+proxy_connect_timeout 1s;
+
+server_tokens off;
+
+server {
+ listen 80 default;
+
+ rewrite ^ https://$host$request_uri permanent;
+}
+
+server {
+ listen 443 default ssl http2;
+
+ ssl_protocols TLSv1.2 TLSv1.3;
+ ssl_dhparam /etc/nginx/ssl/dhparams-4096.pem;
+ ssl_prefer_server_ciphers on;
+
+ ssl_certificate /etc/nginx/ssl/fullchain.pem;
+ ssl_certificate_key /etc/nginx/ssl/privkey.pem;
+
+ include fic-auth.conf;
+
+ root /srv/htdocs-frontend/;
+
+ error_page 401 /welcome.html;
+ error_page 403 404 /e404.html;
+ error_page 413 /e413.html;
+ error_page 500 502 504 /e500.html;
+
+ add_header Strict-Transport-Security max-age=31536000;
+ add_header X-Frame-Options deny;
+ add_header Content-Security-Policy "script-src 'unsafe-inline' 'self' 'unsafe-eval'; img-src 'self' data:; style-src 'unsafe-inline' 'self'; font-src 'self'; default-src 'self'";
+ add_header X-Xss-Protection "1; mode=block";
+ add_header X-Content-Type-Options nosniff;
+ add_header Referrer-Policy strict-origin;
+ add_header Feature-Policy "accelerometer 'none'; ambient-light-sensor 'none'; autoplay 'none'; battery 'none'; camera 'none'; display-capture 'none'; document-domain 'none'; encrypted-media 'none'; fullscreen 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; midi 'none'; payment 'none'; picture-in-picture 'none'; speaker 'none'; sync-xhr 'none'; usb 'none'; vr 'none'; wake-lock 'none'; xr-spatial-tracking 'none'";
+
+ location = / {
+ include fic-get-team.conf;
+ }
+ location = /index.html {
+ include fic-get-team.conf;
+ }
+ location = /welcome.html {
+ internal;
+ if ($http_accept ~ "^application/json") {
+ rewrite ^/(.*).html$ /$1.json;
+ }
+ }
+ location = /e404.html {
+ internal;
+ if ($http_accept ~ "^application/json") {
+ rewrite ^/(.*).html$ /$1.json;
+ }
+ }
+ location = /e413.html {
+ internal;
+ if ($http_accept ~ "^application/json") {
+ rewrite ^/(.*).html$ /$1.json;
+ }
+ }
+ location = /e500.html {
+ internal;
+ if ($http_accept ~ "^application/json") {
+ rewrite ^/(.*).html$ /$1.json;
+ }
+ }
+
+ location ~ ^/([A-Z]|_/) {
+ include fic-get-team.conf;
+
+ rewrite ^/.*$ /index.html;
+ }
+
+ location /edit {
+ include fic-get-team.conf;
+
+ rewrite ^/.*$ /index.html;
+ }
+ location /issues {
+ include fic-get-team.conf;
+
+ rewrite ^/.*$ /index.html;
+ }
+ location /rank {
+ include fic-get-team.conf;
+
+ rewrite ^/.*$ /index.html;
+ }
+ location /tags/ {
+ include fic-get-team.conf;
+
+ rewrite ^/.*$ /index.html;
+ }
+ location /register {
+ include fic-get-team.conf;
+
+ rewrite ^/.*$ /index.html;
+ }
+ location /rules {
+ include fic-get-team.conf;
+
+ rewrite ^/.*$ /index.html;
+ }
+
+ location /files/ {
+ alias /srv/FILES/;
+ sendfile on;
+ tcp_nodelay on;
+ gzip_static always;
+ }
+
+ location /wait.json {
+ include fic-get-team.conf;
+
+ root /srv/TEAMS/$team/;
+ expires epoch;
+ add_header Cache-Control no-cache;
+ }
+ location /stats.json {
+ root /srv/TEAMS/;
+ expires epoch;
+ add_header Cache-Control no-cache;
+ }
+ location /my.json {
+ include fic-get-team.conf;
+
+ root /srv/TEAMS/$team/;
+ expires epoch;
+ add_header Cache-Control no-cache;
+
+ if (!-f /srv/startingblock/started) {
+ rewrite ^/.* /wait.json;
+ }
+ }
+ location /issues.json {
+ include fic-get-team.conf;
+
+ root /srv/TEAMS/$team/;
+ expires epoch;
+ add_header Cache-Control no-cache;
+ }
+ location /scores.json {
+ include fic-get-team.conf;
+
+ root /srv/TEAMS/$team/;
+ expires epoch;
+ add_header Cache-Control no-cache;
+ }
+ location = /events.json {
+ root /srv/TEAMS/;
+ expires epoch;
+ add_header Cache-Control no-cache;
+ }
+ location = /teams.json {
+ root /srv/TEAMS/;
+ expires epoch;
+ add_header Cache-Control no-cache;
+ }
+ location = /themes.json {
+ root /srv/TEAMS/;
+ expires epoch;
+ add_header Cache-Control no-cache;
+ }
+ location = /challenge.json {
+ root /srv/SETTINGSDIST/;
+ expires epoch;
+ add_header X-FIC-time $msec;
+ add_header Cache-Control no-cache;
+ }
+ location = /settings.json {
+ root /srv/SETTINGSDIST/;
+ expires epoch;
+ add_header X-FIC-time $msec;
+ add_header Cache-Control no-cache;
+ }
+
+ location /submit/ {
+ include fic-get-team.conf;
+
+ proxy_pass http://receiver:8080/submission/;
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header X-FIC-Team $team;
+ proxy_redirect off;
+ }
+ location /issue {
+ include fic-get-team.conf;
+
+ proxy_pass http://receiver:8080;
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header X-FIC-Team $team;
+ proxy_redirect off;
+ }
+ location /chname {
+ include fic-get-team.conf;
+
+ proxy_pass http://receiver:8080;
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header X-FIC-Team $team;
+ proxy_redirect off;
+ }
+ location /registration {
+ include fic-get-team.conf;
+
+ proxy_pass http://receiver:8080;
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header X-FIC-Team $team;
+ proxy_redirect off;
+ }
+ location /reset_progress {
+ include fic-get-team.conf;
+
+ proxy_pass http://receiver:8080;
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header X-FIC-Team $team;
+ proxy_redirect off;
+ }
+ location /openhint/ {
+ include fic-get-team.conf;
+
+ proxy_pass http://receiver:8080;
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header X-FIC-Team $team;
+ proxy_redirect off;
+ }
+ location /wantchoices/ {
+ include fic-get-team.conf;
+
+ proxy_pass http://receiver:8080;
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header X-FIC-Team $team;
+ proxy_redirect off;
+ }
+
+ include /etc/nginx/conf.d/*.fic-conf;
+}
diff --git a/configs/nginx/base/static.conf b/configs/nginx/base/static.conf
new file mode 100644
index 00000000..bada9896
--- /dev/null
+++ b/configs/nginx/base/static.conf
@@ -0,0 +1,69 @@
+server {
+ listen 80 default_server;
+ listen [::]:80 default_server;
+
+ server_name fic.srs.epita.fr;
+
+ access_log /var/log/nginx/fic2016.access_log main;
+ error_log /var/log/nginx/fic2016.error_log info;
+
+ root /srv/www/fic2016-static/;
+
+ error_page 403 404 /e404.html;
+ error_page 413 404 /e413.html;
+ error_page 500 502 504 /e500.html;
+
+ location /.htaccess {
+ return 404;
+ }
+ location /chbase.sh {
+ return 404;
+ }
+
+ location ~ ^/[0-9] {
+ rewrite ^/.*$ /index.html;
+ }
+
+ location /edit {
+ rewrite ^/.*$ /index.html;
+ }
+
+ location /rank {
+ rewrite ^/.*$ /index.html;
+ }
+
+ location /tags/ {
+ rewrite ^/.*$ /index.html;
+ }
+
+ location /files/ {
+ sendfile on;
+ tcp_nodelay on;
+ gzip_static always;
+ }
+
+ location = /welcome.html {
+ internal;
+ if ($http_accept ~ "^application/json") {
+ rewrite ^/(.*).html$ /$1.json;
+ }
+ }
+ location = /e404.html {
+ internal;
+ if ($http_accept ~ "^application/json") {
+ rewrite ^/(.*).html$ /$1.json;
+ }
+ }
+ location = /e413.html {
+ internal;
+ if ($http_accept ~ "^application/json") {
+ rewrite ^/(.*).html$ /$1.json;
+ }
+ }
+ location = /e500.html {
+ internal;
+ if ($http_accept ~ "^application/json") {
+ rewrite ^/(.*).html$ /$1.json;
+ }
+ }
+}
diff --git a/configs/nginx/get-team/client-cert.conf b/configs/nginx/get-team/client-cert.conf
new file mode 100644
index 00000000..e0026b09
--- /dev/null
+++ b/configs/nginx/get-team/client-cert.conf
@@ -0,0 +1,19 @@
+set $auth_basic "Challenge FIC";
+if ($ssl_client_verify != "SUCCESS") {
+ set $team "$remote_user";
+ set $needauth "1";
+}
+if ($ssl_client_verify = "SUCCESS") {
+ set $team "_AUTH_ID_$ssl_client_serial";
+ set $auth_basic off;
+ set $needauth "0";
+}
+if (!-f /srv/PKI/shared/ficpasswd) {
+ set $needauth "${needauth}0";
+}
+if ($needauth = "10") {
+ return 401;
+}
+
+auth_basic $auth_basic;
+auth_basic_user_file /srv/PKI/shared/ficpasswd;
diff --git a/configs/nginx/get-team/oidc.conf b/configs/nginx/get-team/oidc.conf
new file mode 100644
index 00000000..32709ab7
--- /dev/null
+++ b/configs/nginx/get-team/oidc.conf
@@ -0,0 +1,3 @@
+auth_request /challenge_access/validate;
+
+auth_request_set $team "$upstream_http_x_vouch_user";
diff --git a/configs/nginx/get-team/request.conf b/configs/nginx/get-team/request.conf
new file mode 100644
index 00000000..b3453f71
--- /dev/null
+++ b/configs/nginx/get-team/request.conf
@@ -0,0 +1,3 @@
+auth_request /auth;
+
+set $team "$remote_user";
diff --git a/configs/nginx/get-team/team-1.conf b/configs/nginx/get-team/team-1.conf
new file mode 100644
index 00000000..a8900e81
--- /dev/null
+++ b/configs/nginx/get-team/team-1.conf
@@ -0,0 +1 @@
+set $team 1;
\ No newline at end of file
diff --git a/configs/nginx/get-team/upstream.conf b/configs/nginx/get-team/upstream.conf
new file mode 100644
index 00000000..370ae10f
--- /dev/null
+++ b/configs/nginx/get-team/upstream.conf
@@ -0,0 +1 @@
+set $team "$http_x_fic_team";
diff --git a/configs/nsenter_iptables.sh b/configs/nsenter_iptables.sh
new file mode 100755
index 00000000..80b6986f
--- /dev/null
+++ b/configs/nsenter_iptables.sh
@@ -0,0 +1,19 @@
+#!/bin/sh
+
+if [ -d /containers/onboot/006-synchro-ip-setup ]; then
+ LOWER=/containers/onboot/006-synchro-ip-setup/lower
+elif [ -d /containers/onboot/006-nginx-ip-setup ]; then
+ LOWER=/containers/onboot/006-nginx-ip-setup/lower
+else
+ nsenter -t 1 -m -u -i -p -- "$0" $@
+ exit $?
+fi
+
+mount -t tmpfs none $LOWER/run
+
+chroot $LOWER iptables $@
+EXIT=$?
+
+umount $LOWER/run
+
+exit ${EXIT}
diff --git a/configs/nsenter_mysql.sh b/configs/nsenter_mysql.sh
new file mode 100755
index 00000000..e3d4b29d
--- /dev/null
+++ b/configs/nsenter_mysql.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+nsenter -t $(pgrep mariadb | head -1) -m -u -i -n -p -- mariadb $@
diff --git a/configs/nsenter_process.sh b/configs/nsenter_process.sh
new file mode 100755
index 00000000..8e78f753
--- /dev/null
+++ b/configs/nsenter_process.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+PROC="$1"
+shift
+
+nsenter -t $(pgrep "$PROC" | head -1) $@
+exit $?
diff --git a/configs/pxelinux.cfg b/configs/pxelinux.cfg
new file mode 100644
index 00000000..8f9a58b7
--- /dev/null
+++ b/configs/pxelinux.cfg
@@ -0,0 +1,35 @@
+TIMEOUT 30
+ONTIMEOUT update
+
+MENU background #00000000 * *
+MENU color title * #FF22BBCC *
+MENU color sel * #FFFFFFFF #FF22BBCC *
+MENU color hotsel 1;7;37;40 #ffffffff #76a1d0ff *
+
+UI vesamenu.c32
+MENU TITLE FICKIT PXE BOOT
+
+LABEL backend
+ MENU LABEL Prepare for ^backend
+ LINUX /s/fickit-boot-kernel
+ INITRD /s/fickit-prepare-initrd.img
+ APPEND console=tty0 fickit.autoprepare=backend
+LABEL frontend
+ MENU LABEL Prepare for ^frontend
+ LINUX /s/fickit-boot-kernel
+ INITRD /s/fickit-prepare-initrd.img
+ APPEND console=tty0 fickit.autoprepare=frontend
+LABEL prepare
+ MENU LABEL Prepare with ^shell
+ LINUX /s/fickit-boot-kernel
+ INITRD /s/fickit-prepare-initrd.img
+ APPEND console=tty0
+LABEL update
+ MENU LABEL ^Update images
+ LINUX /s/fickit-boot-kernel
+ INITRD /s/fickit-update-initrd.img
+ APPEND console=ttyS0 console=tty0
+MENU SEPARATOR
+LABEL poweroff
+ MENU LABEL ^Shutdown
+ KERNEL poweroff.c32
diff --git a/configs/sshd-setup.sh b/configs/sshd-setup.sh
new file mode 100644
index 00000000..e5f969f8
--- /dev/null
+++ b/configs/sshd-setup.sh
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+[ -f /var/lib/fic/ssh/sshd_config ] && exit 0
+
+mkdir -p /var/lib/fic/ssh/
+
+cp /containers/services/sshd/rootfs/etc/ssh/* /var/lib/fic/ssh/
+
+mount -o bind /dev /containers/services/sshd/rootfs/dev
+mount -o bind /proc /containers/services/sshd/rootfs/proc
+mount -o bind /sys /containers/services/sshd/rootfs/sys
+mount -o bind /var/lib/fic/ssh/ /containers/services/sshd/rootfs/etc/ssh
+
+chroot /containers/services/sshd/rootfs/ ssh-keygen -A
+
+umount /containers/services/sshd/rootfs/dev /containers/services/sshd/rootfs/proc /containers/services/sshd/rootfs/sys /containers/services/sshd/rootfs/etc/ssh
diff --git a/configs/synchro.sh b/configs/synchro.sh
new file mode 100755
index 00000000..8fd8f8df
--- /dev/null
+++ b/configs/synchro.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+
+# This script synchronizes first, the generated frontend and then
+# retrieves submissions
+
+BASEDIR="/srv"
+FRONTEND_HOSTNAME="phobos"
+
+SSH_OPTS="ssh -p 22 -i ~/.ssh/id_ed25519 -o ControlMaster=auto -o ControlPath=/root/.ssh/%r@%h:%p -o ControlPersist=2 -o PasswordAuthentication=no -o StrictHostKeyChecking=no"
+
+cd "${BASEDIR}"
+
+touch /tmp/stop
+
+# Establish first ssh connection for controlpersist socket, to avoid delay during time synchronization
+${SSH_OPTS} ${FRONTEND_HOSTNAME} ls > /dev/null
+
+# Synchronize the date one time
+${SSH_OPTS} ${FRONTEND_HOSTNAME} date -s @"$(date +%s)"
+
+# Synchronize static files in a separate loop (to avoid submissions delays during file synchronization)
+while ! [ -f SETTINGS/stop ] || [ /tmp/stop -nt SETTINGS/stop ]
+do
+ rsync -e "$SSH_OPTS" -av --delete FILES "${FRONTEND_HOSTNAME}":"${BASEDIR}"
+
+ # Synchronize logs
+ rsync -e "$SSH_OPTS" -av "${FRONTEND_HOSTNAME}":/var/log/ /var/log/frontend
+
+ sleep 5
+done &
+
+while ! [ -f SETTINGS/stop ] || [ /tmp/stop -nt SETTINGS/stop ]
+do
+ # Synchronize static files pages
+ rsync -e "$SSH_OPTS" -av --delete --delay-updates --partial-dir=.tmp/ PKI TEAMS SETTINGSDIST "${FRONTEND_HOSTNAME}":"${BASEDIR}"
+
+ # Synchronize submissions
+ rsync -e "$SSH_OPTS" -av --ignore-existing --delay-updates --temp-dir=.tmp/ --partial-dir=.tmp/ --remove-source-files "${FRONTEND_HOSTNAME}":"${BASEDIR}"/submissions/ submissions/
+
+ sleep 0.3
+done
+
+wait
+echo See you
diff --git a/configs/sysctl-backend.conf b/configs/sysctl-backend.conf
new file mode 100644
index 00000000..50ca3f42
--- /dev/null
+++ b/configs/sysctl-backend.conf
@@ -0,0 +1,7 @@
+net.ipv6.conf.all.disable_ipv6 = 1
+
+# Increase system file descriptor limit
+fs.file-max = 65535
+
+# Increase system IP port limits
+net.ipv4.ip_local_port_range = 2000 65000
diff --git a/configs/sysctl-frontend.conf b/configs/sysctl-frontend.conf
new file mode 100644
index 00000000..50ca3f42
--- /dev/null
+++ b/configs/sysctl-frontend.conf
@@ -0,0 +1,7 @@
+net.ipv6.conf.all.disable_ipv6 = 1
+
+# Increase system file descriptor limit
+fs.file-max = 65535
+
+# Increase system IP port limits
+net.ipv4.ip_local_port_range = 2000 65000
diff --git a/configs/udhcpd-sample.conf b/configs/udhcpd-sample.conf
new file mode 100644
index 00000000..30f9b119
--- /dev/null
+++ b/configs/udhcpd-sample.conf
@@ -0,0 +1,59 @@
+# Sample udhcpd configuration file (/etc/udhcpd.conf)
+# Values shown are defaults
+
+# The start and end of the IP lease block
+start 192.168.255.100
+end 192.168.255.200
+
+# The interface that udhcpd will use
+interface eth0
+
+# The maximum number of leases (includes addresses reserved
+# by OFFER's, DECLINE's, and ARP conflicts). Will be corrected
+# if it's bigger than IP lease block, but it ok to make it
+# smaller than lease block.
+max_leases 100
+
+# The amount of time that an IP will be reserved (leased to nobody)
+# if a DHCP decline message is received (seconds)
+#decline_time 3600
+
+# The amount of time that an IP will be reserved
+# if an ARP conflict occurs (seconds)
+#conflict_time 3600
+
+# How long an offered address is reserved (seconds)
+#offer_time 60
+
+# If client asks for lease below this value, it will be rounded up
+# to this value (seconds)
+#min_lease 60
+
+# The location of the pid file
+#pidfile /var/run/udhcpd.pid
+
+# The location of the leases file
+#lease_file /var/lib/misc/udhcpd.leases
+
+# The following are BOOTP specific options
+# next server to use in bootstrap
+siaddr 192.168.255.2 # default: 0.0.0.0 (none)
+# tftp server name
+#sname zorak # default: none
+# tftp file to download (e.g. kernel image)
+boot_file pxelinux.0 # default: none
+
+# NOTE: "boot_file FILE" and "opt bootfile FILE" are conceptually the same,
+# but "boot_file" goes into BOOTP-defined fixed-size field in the packet,
+# whereas "opt bootfile" goes into DHCP option 0x43.
+# Same for "sname HOST" and "opt tftp HOST".
+
+# The remainder of options are DHCP options and can be specified with the
+# keyword 'opt' or 'option'. If an option can take multiple items, such
+# as the dns option, they can be listed on the same line, or multiple
+# lines.
+# Examples:
+#opt dns 192.168.10.2 192.168.10.10
+option subnet 255.255.255.0
+opt router 192.168.255.2
+option lease 3600
diff --git a/configs/update-backend.sh b/configs/update-backend.sh
new file mode 100755
index 00000000..efeb1aab
--- /dev/null
+++ b/configs/update-backend.sh
@@ -0,0 +1,11 @@
+#!/bin/sh
+
+IP_BACKEND=192.168.3.92
+IMG_BACKEND=fickit-backend-squashfs.img
+IMG_METADATA=fickit-metadata.iso
+
+echo "Sending image..."
+rsync -v -e ssh "${IMG_BACKEND}" "${IMG_METADATA}" "root@${IP_BACKEND}:/var/lib/fic/outofsync/" || exit 1
+
+echo "Done!"
+echo "Now, execute upgrade_image on backend, through iDRAC interface."
diff --git a/configs/update_imgs.sh b/configs/update_imgs.sh
new file mode 100644
index 00000000..fa507cee
--- /dev/null
+++ b/configs/update_imgs.sh
@@ -0,0 +1,37 @@
+#!/bin/sh
+
+mkdir -p /boot/imgs
+
+# Backup the previous metadata
+/usr/bin/metadata -v cdrom
+mv /boot/imgs/fickit-metadata.iso /boot/imgs/fickit-metadata.iso.bak
+
+for img in fickit-boot-kernel fickit-metadata.iso fickit-boot-initrd.img fickit-prepare-initrd.img fickit-frontend-squashfs.img fickit-backend-squashfs.img fickit-update-initrd.img
+do
+ wget -O "/boot/imgs/${img}" "$1/${img}"
+done
+
+# Check dm-crypt key not changed
+ISO=$(mktemp -d)
+mount /boot/imgs/fickit-metadata.iso "${ISO}"
+
+NEW_KEY=$(sed -rn 's/.*"content": "([^"]+)"$/\1/p' "${ISO}/user-data" | head -n 1)
+OLD_KEY=$(cat /run/config/dm-crypt/key)
+
+[ "${NEW_KEY}" != "${OLD_KEY}" ] && {
+ read -p "DM-CRYPT key changed in metadata, are you sure you want to erase it? (y/N) " V
+ [ "$V" != "y" ] && [ "$V" != "Y" ] && while true; do
+ mv /boot/imgs/fickit-metadata.iso /boot/imgs/fickit-metadata.iso.skipped
+ cp /boot/imgs/fickit-metadata.iso.bak /boot/imgs/fickit-metadata.iso
+ echo
+ echo "Metadata drive not erased"
+ echo
+ /bin/ash
+ sync
+ reboot -f
+ done
+}
+
+umount "${ISO}"
+
+dd if=/boot/imgs/fickit-metadata.iso of="$2"
diff --git a/dashboard/.gitignore b/dashboard/.gitignore
new file mode 100644
index 00000000..ef360c84
--- /dev/null
+++ b/dashboard/.gitignore
@@ -0,0 +1 @@
+dashboard
\ No newline at end of file
diff --git a/dashboard/app.go b/dashboard/app.go
new file mode 100644
index 00000000..6f6ecaaf
--- /dev/null
+++ b/dashboard/app.go
@@ -0,0 +1,75 @@
+package main
+
+import (
+ "context"
+ "log"
+ "net/http"
+ "time"
+
+ "github.com/gin-gonic/gin"
+)
+
+type App struct {
+ router *gin.Engine
+ srv *http.Server
+ bind string
+ ips []string
+}
+
+func NewApp(htpasswd_file *string, restrict_to_ips *string, baseURL string, bind string) App {
+ gin.ForceConsoleColor()
+ router := gin.Default()
+
+ var baserouter *gin.RouterGroup
+ if len(baseURL) > 1 {
+ router.GET("/", func(c *gin.Context) {
+ c.Redirect(http.StatusFound, baseURL)
+ })
+
+ baserouter = router.Group(baseURL)
+ } else {
+ baserouter = router.Group("")
+ }
+
+ if htpasswd_file != nil && len(*htpasswd_file) > 0 {
+ baserouter.Use(Htpassword(*htpasswd_file))
+ }
+
+ app := App{
+ router: router,
+ bind: bind,
+ }
+
+ if restrict_to_ips != nil && len(*restrict_to_ips) > 0 {
+ app.loadAndWatchIPs(*restrict_to_ips)
+ baserouter.Use(app.Restrict2IPs)
+ }
+
+ declareStaticRoutes(baserouter, baseURL)
+
+ return app
+}
+
+func (app *App) Start() {
+ app.srv = &http.Server{
+ Addr: app.bind,
+ Handler: app.router,
+ ReadHeaderTimeout: 15 * time.Second,
+ ReadTimeout: 15 * time.Second,
+ WriteTimeout: 10 * time.Second,
+ IdleTimeout: 30 * time.Second,
+ }
+
+ log.Printf("Ready, listening on %s\n", app.bind)
+ if err := app.srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
+ log.Fatalf("listen: %s\n", err)
+ }
+}
+
+func (app *App) Stop() {
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ if err := app.srv.Shutdown(ctx); err != nil {
+ log.Fatal("Server Shutdown:", err)
+ }
+}
diff --git a/dashboard/fwd.go b/dashboard/fwd.go
new file mode 100644
index 00000000..9514a134
--- /dev/null
+++ b/dashboard/fwd.go
@@ -0,0 +1,50 @@
+package main
+
+import (
+ "io"
+ "net/http"
+ "net/url"
+ "os"
+ "path"
+)
+
+func fwd_request(w http.ResponseWriter, r *http.Request, fwd string) {
+ if u, err := url.Parse(fwd); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ } else {
+ var user, pass string
+ if u.User != nil {
+ user = u.User.Username()
+ pass, _ = u.User.Password()
+ u.User = nil
+ }
+ if v, exists := os.LookupEnv("FICCLOUD_USER"); exists {
+ user = v
+ } else if v, exists := os.LookupEnv("FICCLOUD_PASS"); exists {
+ pass = v
+ }
+
+ u.Path = path.Join(u.Path, r.URL.Path)
+
+ if r, err := http.NewRequest(r.Method, u.String(), r.Body); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ } else {
+ if len(user) != 0 || len(pass) != 0 {
+ r.SetBasicAuth(user, pass)
+ }
+
+ if resp, err := http.DefaultClient.Do(r); err != nil {
+ http.Error(w, err.Error(), http.StatusBadGateway)
+ } else {
+ defer resp.Body.Close()
+
+ for key := range resp.Header {
+ w.Header().Add(key, resp.Header.Get(key))
+ }
+ w.WriteHeader(resp.StatusCode)
+
+ io.Copy(w, resp.Body)
+ }
+ }
+ }
+}
diff --git a/dashboard/htpasswd.go b/dashboard/htpasswd.go
new file mode 100644
index 00000000..1f62bf9a
--- /dev/null
+++ b/dashboard/htpasswd.go
@@ -0,0 +1,78 @@
+package main
+
+import (
+ "bufio"
+ "fmt"
+ "log"
+ "net/http"
+ "os"
+ "strings"
+
+ "github.com/gin-gonic/gin"
+ "gitlab.com/nyarla/go-crypt"
+)
+
+func Htpassword(file string) func(c *gin.Context) {
+ htpasswd := &Htpasswd{}
+
+ log.Println("Reading htpasswd file...")
+ var err error
+ if htpasswd, err = NewHtpasswd(file); htpasswd == nil {
+ log.Fatal("Unable to parse htpasswd:", err)
+ }
+
+ return func(c *gin.Context) {
+ username, password, ok := c.Request.BasicAuth()
+ if !ok {
+ c.Writer.Header().Add("WWW-Authenticate", "Basic realm=\"FIC challenge Dashboard\"")
+ c.AbortWithError(http.StatusUnauthorized, fmt.Errorf("Please login"))
+ return
+ }
+
+ if !htpasswd.Authenticate(username, password) {
+ c.Writer.Header().Add("WWW-Authenticate", "Basic realm=\"FIC challenge Dashboard\"")
+ c.AbortWithError(http.StatusUnauthorized, fmt.Errorf("Not authorized"))
+ return
+ }
+
+ c.Next()
+ }
+}
+
+type Htpasswd struct {
+ entries map[string]string
+}
+
+func NewHtpasswd(path string) (*Htpasswd, error) {
+ if fd, err := os.Open(path); err != nil {
+ return nil, err
+ } else {
+ defer fd.Close()
+
+ htpasswd := Htpasswd{
+ map[string]string{},
+ }
+
+ scanner := bufio.NewScanner(fd)
+ for scanner.Scan() {
+ line := strings.SplitN(strings.TrimSpace(scanner.Text()), ":", 2)
+ if len(line) == 2 && len(line[1]) > 2 {
+ htpasswd.entries[line[0]] = line[1]
+ }
+ }
+ if err := scanner.Err(); err != nil {
+ return nil, err
+ }
+ return &htpasswd, nil
+ }
+}
+
+func (h Htpasswd) Authenticate(username, password string) bool {
+ if hash, ok := h.entries[username]; !ok {
+ return false
+ } else if crypt.Crypt(password, hash[:2]) != hash {
+ return false
+ } else {
+ return true
+ }
+}
diff --git a/dashboard/main.go b/dashboard/main.go
new file mode 100644
index 00000000..4c6de9e0
--- /dev/null
+++ b/dashboard/main.go
@@ -0,0 +1,90 @@
+package main
+
+import (
+ "flag"
+ "io/fs"
+ "log"
+ "net/http"
+ "os"
+ "os/signal"
+ "path"
+ "path/filepath"
+ "syscall"
+
+ "srs.epita.fr/fic-server/libfic"
+ "srs.epita.fr/fic-server/settings"
+)
+
+var DashboardDir string
+var TeamsDir string
+
+func main() {
+ var baseURL string
+ // Read paremeters from environment
+ if v, exists := os.LookupEnv("FIC_BASEURL"); exists {
+ baseURL = v
+ }
+
+ // Read parameters from command line
+ var bind = flag.String("bind", "127.0.0.1:8082", "Bind port/socket")
+ htpasswd_file := flag.String("htpasswd", "", "Restrict access with password, Apache htpasswd format")
+ restrict_ip := flag.String("restrict-to-ips", "", "Restrict access to IP listed in this JSON array")
+ flag.StringVar(&baseURL, "baseurl", baseURL, "URL prepended to each URL")
+ staticDir := flag.String("static", "./htdocs-dashboard/", "Directory containing static files")
+ flag.StringVar(&fic.FilesDir, "files", fic.FilesDir, "Base directory where found challenges files, local part")
+ flag.StringVar(&DashboardDir, "dashbord", "./DASHBOARD", "Base directory where save public JSON files")
+ flag.StringVar(&TeamsDir, "teams", "./TEAMS", "Base directory where save teams JSON files")
+ flag.StringVar(&settings.SettingsDir, "settings", "./SETTINGSDIST", "Base directory where load and save settings")
+ var fwdr = flag.String("forwarder", "", "URL of another dashboard where send traffic to, except static assets")
+ flag.BoolVar(&fwdPublicJson, "fwdpublicjson", fwdPublicJson, "Also forward public.json files to forwarder")
+ flag.Parse()
+
+ log.SetPrefix("[public] ")
+
+ // Sanitize options
+ var err error
+ log.Println("Checking paths...")
+ if staticDir != nil && *staticDir != "" {
+ if sDir, err := filepath.Abs(*staticDir); err != nil {
+ log.Fatal(err)
+ } else {
+ log.Println("Serving pages from", sDir)
+ staticFS = http.Dir(sDir)
+ }
+ } else {
+ sub, err := fs.Sub(assets, "static")
+ if err != nil {
+ log.Fatal("Unable to cd to static/ directory:", err)
+ }
+ log.Println("Serving pages from memory.")
+ staticFS = http.FS(sub)
+ }
+ if fic.FilesDir, err = filepath.Abs(fic.FilesDir); err != nil {
+ log.Fatal(err)
+ }
+ if settings.SettingsDir, err = filepath.Abs(settings.SettingsDir); err != nil {
+ log.Fatal(err)
+ }
+ if baseURL != "/" {
+ baseURL = path.Clean(baseURL)
+ } else {
+ baseURL = ""
+ }
+ if fwdr != nil && len(*fwdr) > 0 {
+ forwarder = fwdr
+ }
+
+ // Prepare graceful shutdown
+ interrupt := make(chan os.Signal, 1)
+ signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM)
+
+ app := NewApp(htpasswd_file, restrict_ip, baseURL, *bind)
+ go app.Start()
+
+ // Wait shutdown signal
+ <-interrupt
+
+ log.Print("The service is shutting down...")
+ app.Stop()
+ log.Println("done")
+}
diff --git a/dashboard/restrict_ip.go b/dashboard/restrict_ip.go
new file mode 100644
index 00000000..e2ec172c
--- /dev/null
+++ b/dashboard/restrict_ip.go
@@ -0,0 +1,99 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "log"
+ "net/http"
+ "os"
+ "os/signal"
+ "path"
+ "strings"
+ "syscall"
+
+ "github.com/gin-gonic/gin"
+ "gopkg.in/fsnotify.v1"
+)
+
+func (app *App) loadAndWatchIPs(ipfile string) {
+ // First load of file if it exists
+ app.tryReloadIPList(ipfile)
+
+ // Register SIGHUP
+ c := make(chan os.Signal, 1)
+ signal.Notify(c, syscall.SIGHUP)
+ go func() {
+ for range c {
+ log.Println("SIGHUP received, reloading ip list...")
+ go app.tryReloadIPList(ipfile)
+ }
+ }()
+
+ // Watch the configuration file
+ if watcher, err := fsnotify.NewWatcher(); err != nil {
+ log.Fatal(err)
+ } else {
+ if err := watcher.Add(path.Dir(ipfile)); err != nil {
+ log.Fatal("Unable to watch: ", path.Dir(ipfile), ": ", err)
+ }
+
+ go func() {
+ defer watcher.Close()
+ for {
+ select {
+ case ev := <-watcher.Events:
+ if path.Base(ev.Name) == path.Base(ipfile) && ev.Op&(fsnotify.Write|fsnotify.Create) != 0 {
+ log.Println("IPs file changes, reloading them!")
+ go app.tryReloadIPList(ipfile)
+ }
+ case err := <-watcher.Errors:
+ log.Println("watcher error:", err)
+ }
+ }
+ }()
+ }
+}
+
+func (app *App) tryReloadIPList(ipfile string) {
+ if _, err := os.Stat(ipfile); !os.IsNotExist(err) {
+ if iplist, err := loadIPs(ipfile); err != nil {
+ log.Println("ERROR: Unable to read ip file:", err)
+ } else {
+ log.Printf("Loading %d IPs to authorized list", len(iplist))
+ app.ips = iplist
+ }
+ }
+}
+
+func loadIPs(file string) ([]string, error) {
+ fd, err := os.Open(file)
+ if err != nil {
+ return nil, err
+ }
+ defer fd.Close()
+
+ var ret []string
+ jdec := json.NewDecoder(fd)
+
+ if err := jdec.Decode(&ret); err != nil {
+ return ret, err
+ }
+
+ return ret, nil
+}
+
+func (app *App) Restrict2IPs(c *gin.Context) {
+ found := false
+ for _, ip := range app.ips {
+ if strings.HasPrefix(c.Request.RemoteAddr, ip) {
+ found = true
+ break
+ }
+ }
+
+ if found {
+ c.Next()
+ } else {
+ c.AbortWithError(http.StatusForbidden, fmt.Errorf("IP not authorized: %s", c.Request.RemoteAddr))
+ }
+}
diff --git a/dashboard/static.go b/dashboard/static.go
new file mode 100644
index 00000000..55308ce0
--- /dev/null
+++ b/dashboard/static.go
@@ -0,0 +1,248 @@
+package main
+
+import (
+ "bytes"
+ "embed"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "path"
+ "strings"
+ "time"
+
+ "srs.epita.fr/fic-server/libfic"
+ "srs.epita.fr/fic-server/settings"
+
+ "github.com/gin-gonic/gin"
+)
+
+//go:embed static
+
+var assets embed.FS
+var staticFS http.FileSystem
+
+var forwarder *string = nil
+var fwdPublicJson = false
+
+var indexTmpl []byte
+
+func getIndexHtml(w io.Writer, baseURL string) {
+ if len(indexTmpl) == 0 {
+ if file, err := staticFS.Open("index.html"); err != nil {
+ log.Println("Unable to open index.html: ", err)
+ } else {
+ defer file.Close()
+
+ if indexTmpl, err = ioutil.ReadAll(file); err != nil {
+ log.Println("Cannot read whole index.html: ", err)
+ } else {
+ indexTmpl = bytes.Replace(indexTmpl, []byte("{{.urlbase}}"), []byte(path.Clean(path.Join(baseURL+"/", "nuke"))[:len(path.Clean(path.Join(baseURL+"/", "nuke")))-4]), -1)
+ }
+ }
+ }
+
+ w.Write(indexTmpl)
+}
+
+func serveFile(c *gin.Context, url string) {
+ c.Request.URL.Path = url
+ http.FileServer(staticFS).ServeHTTP(c.Writer, c.Request)
+}
+
+func declareStaticRoutes(router *gin.RouterGroup, baseURL string) {
+ router.GET("/", func(c *gin.Context) {
+ http.Redirect(c.Writer, c.Request, "public0.html", http.StatusFound)
+ })
+ for i := 0; i <= 9; i++ {
+ router.GET(fmt.Sprintf("/public%d.html", i), func(c *gin.Context) {
+ getIndexHtml(c.Writer, baseURL)
+ })
+ }
+
+ router.GET("/css/*_", func(c *gin.Context) {
+ serveFile(c, strings.TrimPrefix(c.Request.URL.Path, baseURL))
+ })
+ router.GET("/fonts/*_", func(c *gin.Context) {
+ serveFile(c, strings.TrimPrefix(c.Request.URL.Path, baseURL))
+ })
+ router.GET("/img/*_", func(c *gin.Context) {
+ serveFile(c, strings.TrimPrefix(c.Request.URL.Path, baseURL))
+ })
+ router.GET("/js/*_", func(c *gin.Context) {
+ serveFile(c, strings.TrimPrefix(c.Request.URL.Path, baseURL))
+ })
+ router.GET("/views/*_", func(c *gin.Context) {
+ serveFile(c, strings.TrimPrefix(c.Request.URL.Path, baseURL))
+ })
+
+ router.GET("/files/*_", func(c *gin.Context) {
+ if forwarder != nil {
+ fwd_request(c.Writer, c.Request, *forwarder)
+ } else {
+ http.ServeFile(c.Writer, c.Request, path.Join(fic.FilesDir, strings.TrimPrefix(c.Request.URL.Path, "/"+path.Join(baseURL, "files"))))
+ }
+ })
+
+ router.GET("/events.json", func(c *gin.Context) {
+ c.Writer.Header().Set("Cache-Control", "no-cache")
+ if forwarder != nil {
+ fwd_request(c.Writer, c.Request, *forwarder)
+ } else {
+ http.ServeFile(c.Writer, c.Request, path.Join(TeamsDir, "events.json"))
+ }
+ })
+ router.GET("/my.json", func(c *gin.Context) {
+ c.Writer.Header().Set("Cache-Control", "no-cache")
+ if forwarder != nil {
+ fwd_request(c.Writer, c.Request, *forwarder)
+ } else {
+ http.ServeFile(c.Writer, c.Request, path.Join(TeamsDir, "public", "my.json"))
+ }
+ })
+ router.GET("/api/teams/:tid/score-grid.json", func(c *gin.Context) {
+ if forwarder != nil {
+ fwd_request(c.Writer, c.Request, *forwarder)
+ } else {
+ fwd_request(c.Writer, c.Request, "http://127.0.0.1:8081/")
+ }
+ })
+ router.GET("/api/teams/:tid/stats.json", func(c *gin.Context) {
+ if forwarder != nil {
+ fwd_request(c.Writer, c.Request, *forwarder)
+ } else {
+ fwd_request(c.Writer, c.Request, "http://127.0.0.1:8081/")
+ }
+ })
+ router.GET("/challenge.json", func(c *gin.Context) {
+ c.Writer.Header().Set("Cache-Control", "no-cache")
+ if forwarder != nil {
+ fwd_request(c.Writer, c.Request, *forwarder)
+ } else {
+ http.ServeFile(c.Writer, c.Request, path.Join(settings.SettingsDir, settings.ChallengeFile))
+ }
+ })
+ router.GET("/settings.json", func(c *gin.Context) {
+ c.Writer.Header().Set("Cache-Control", "no-cache")
+ if forwarder != nil {
+ fwd_request(c.Writer, c.Request, *forwarder)
+ } else {
+ c.Writer.Header().Set("X-FIC-Time", fmt.Sprintf("%f", float64(time.Now().UnixNano()/1000)/1000000))
+ http.ServeFile(c.Writer, c.Request, path.Join(settings.SettingsDir, settings.SettingsFile))
+ }
+ })
+ router.GET("/teams.json", func(c *gin.Context) {
+ c.Writer.Header().Set("Cache-Control", "no-cache")
+ if forwarder != nil {
+ fwd_request(c.Writer, c.Request, *forwarder)
+ } else {
+ http.ServeFile(c.Writer, c.Request, path.Join(TeamsDir, "public", "teams.json"))
+ }
+ })
+ router.GET("/themes.json", func(c *gin.Context) {
+ c.Writer.Header().Set("Cache-Control", "no-cache")
+ if forwarder != nil {
+ fwd_request(c.Writer, c.Request, *forwarder)
+ } else {
+ http.ServeFile(c.Writer, c.Request, path.Join(TeamsDir, "themes.json"))
+ }
+ })
+
+ router.GET("/background.png", func(c *gin.Context) {
+ if forwarder != nil && fwdPublicJson {
+ fwd_request(c.Writer, c.Request, *forwarder)
+ } else {
+ http.ServeFile(c.Writer, c.Request, path.Join(DashboardDir, "background.png"))
+ }
+ })
+
+ router.GET("/public.json", func(c *gin.Context) {
+ c.Writer.Header().Set("Cache-Control", "no-cache")
+ if forwarder != nil && fwdPublicJson {
+ fwd_request(c.Writer, c.Request, *forwarder)
+ } else {
+ http.ServeFile(c.Writer, c.Request, path.Join(DashboardDir, "public.json"))
+ }
+ })
+ router.GET("/public0.json", func(c *gin.Context) {
+ c.Writer.Header().Set("Cache-Control", "no-cache")
+ if forwarder != nil && fwdPublicJson {
+ fwd_request(c.Writer, c.Request, *forwarder)
+ } else {
+ http.ServeFile(c.Writer, c.Request, path.Join(DashboardDir, "public0.json"))
+ }
+ })
+ router.GET("/public1.json", func(c *gin.Context) {
+ c.Writer.Header().Set("Cache-Control", "no-cache")
+ if forwarder != nil && fwdPublicJson {
+ fwd_request(c.Writer, c.Request, *forwarder)
+ } else {
+ http.ServeFile(c.Writer, c.Request, path.Join(DashboardDir, "public1.json"))
+ }
+ })
+ router.GET("/public2.json", func(c *gin.Context) {
+ c.Writer.Header().Set("Cache-Control", "no-cache")
+ if forwarder != nil && fwdPublicJson {
+ fwd_request(c.Writer, c.Request, *forwarder)
+ } else {
+ http.ServeFile(c.Writer, c.Request, path.Join(DashboardDir, "public2.json"))
+ }
+ })
+ router.GET("/public3.json", func(c *gin.Context) {
+ c.Writer.Header().Set("Cache-Control", "no-cache")
+ if forwarder != nil && fwdPublicJson {
+ fwd_request(c.Writer, c.Request, *forwarder)
+ } else {
+ http.ServeFile(c.Writer, c.Request, path.Join(DashboardDir, "public3.json"))
+ }
+ })
+ router.GET("/public4.json", func(c *gin.Context) {
+ c.Writer.Header().Set("Cache-Control", "no-cache")
+ if forwarder != nil && fwdPublicJson {
+ fwd_request(c.Writer, c.Request, *forwarder)
+ } else {
+ http.ServeFile(c.Writer, c.Request, path.Join(DashboardDir, "public4.json"))
+ }
+ })
+ router.GET("/public5.json", func(c *gin.Context) {
+ c.Writer.Header().Set("Cache-Control", "no-cache")
+ if forwarder != nil && fwdPublicJson {
+ fwd_request(c.Writer, c.Request, *forwarder)
+ } else {
+ http.ServeFile(c.Writer, c.Request, path.Join(DashboardDir, "public5.json"))
+ }
+ })
+ router.GET("/public6.json", func(c *gin.Context) {
+ c.Writer.Header().Set("Cache-Control", "no-cache")
+ if forwarder != nil && fwdPublicJson {
+ fwd_request(c.Writer, c.Request, *forwarder)
+ } else {
+ http.ServeFile(c.Writer, c.Request, path.Join(DashboardDir, "public6.json"))
+ }
+ })
+ router.GET("/public7.json", func(c *gin.Context) {
+ c.Writer.Header().Set("Cache-Control", "no-cache")
+ if forwarder != nil && fwdPublicJson {
+ fwd_request(c.Writer, c.Request, *forwarder)
+ } else {
+ http.ServeFile(c.Writer, c.Request, path.Join(DashboardDir, "public7.json"))
+ }
+ })
+ router.GET("/public8.json", func(c *gin.Context) {
+ c.Writer.Header().Set("Cache-Control", "no-cache")
+ if forwarder != nil && fwdPublicJson {
+ fwd_request(c.Writer, c.Request, *forwarder)
+ } else {
+ http.ServeFile(c.Writer, c.Request, path.Join(DashboardDir, "public8.json"))
+ }
+ })
+ router.GET("/public9.json", func(c *gin.Context) {
+ c.Writer.Header().Set("Cache-Control", "no-cache")
+ if forwarder != nil && fwdPublicJson {
+ fwd_request(c.Writer, c.Request, *forwarder)
+ } else {
+ http.ServeFile(c.Writer, c.Request, path.Join(DashboardDir, "public9.json"))
+ }
+ })
+}
diff --git a/dashboard/static/css/bootstrap.min.css b/dashboard/static/css/bootstrap.min.css
new file mode 120000
index 00000000..8dd0a066
--- /dev/null
+++ b/dashboard/static/css/bootstrap.min.css
@@ -0,0 +1 @@
+../../../admin/static/css/bootstrap.min.css
\ No newline at end of file
diff --git a/dashboard/static/css/fic.css b/dashboard/static/css/fic.css
new file mode 100644
index 00000000..9c88d5ca
--- /dev/null
+++ b/dashboard/static/css/fic.css
@@ -0,0 +1,387 @@
+@font-face {
+ font-family: "Linux Biolinum";
+ src: url('../fonts/LinBiolinum_R.woff') format('woff');
+}
+@font-face {
+ font-family: "Linux Biolinum";
+ src: url('../fonts/LinBiolinum_RB.woff') format('woff');
+ font-weight: bold;
+}
+@font-face {
+ font-family: "Linux Biolinum";
+ src: url('../fonts/LinBiolinum_RI.woff') format('woff');
+ font-style: italic;
+}
+@font-face {
+ font-family: 'FantasqueSansMonoRegular';
+ src: url('../fonts/FantasqueSansMono-Regular.woff') format('woff');
+ font-weight: normal;
+ font-style: normal;
+}
+
+b, strong {
+ font-weight: bold;
+}
+
+[ng-cloak] {
+ display:none !important;
+}
+
+.popover.bs-popover-left .arrow::after {
+ border-left-color: #7A8288;
+}
+
+body {
+ overflow-y: scroll;
+}
+
+.bg-public {
+ background-image: url('../background.png');
+ background-repeat: no-repeat;
+ background-size: contain;
+ height: 100vh;
+}
+
+.bg-public .carousel h3 {
+ font-size: 1.5rem;
+ line-height: 1.1rem;
+}
+
+.flag {
+ font-family: 'FantasqueSansMonoRegular', monospace;
+}
+
+.card-img-top {
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: cover;
+}
+.theme-card {
+ height: 10rem;
+}
+
+.beautiful {
+ font-family: "Linux Biolinum",Helvetica,Arial,sans-serif;
+}
+.beautiful ol {
+ font-size: 133%;
+}
+.beautiful ol ol {
+ font-size: 90%;
+}
+
+.text-bold {
+ font-weight: bolder;
+}
+.text-indent p {
+ text-indent: 1em;
+}
+
+.navbar {
+ margin-bottom: 0;
+}
+.niceborder {
+ border-bottom: 5px #4eaee6 solid;
+}
+.navbar img {
+ margin: 3px auto;
+ height: 100px;
+}
+.navbar .clock {
+ font-size: 70px;
+}
+.clock:not(.expired):not(.wait) .point, .clock.expired {
+ transition: color text-shadow 1s;
+ position: relative;
+ animation: clockanim 1s ease infinite;
+ -moz-animation: clockanim 1s ease infinite;
+ -webkit-animation: clockanim 1s ease infinite;
+}
+.clock.wait .point {
+ transition: color text-shadow 1s;
+ position: relative;
+ animation: clockwait 1s ease infinite;
+ -moz-animation: clockwait 1s ease infinite;
+ -webkit-animation: clockwait 1s ease infinite;
+}
+.end {
+ color: #e64143;
+}
+.point {
+ text-shadow: 0 0 20px #4eaee6;
+}
+.end .point {
+ text-shadow: 0 0 20px #e64143;
+}
+@-webkit-keyframes clockanim {
+ 0% { opacity: 1.0; }
+ 50% { opacity: 0; }
+ 100% { opacity: 1.0; };
+}
+@-moz-keyframes clockanim {
+ 0% { opacity: 1.0; }
+ 50% { opacity: 0; }
+ 100% { opacity: 1.0; };
+}
+keyframes clockanim {
+ 0% { opacity: 1.0; }
+ 50% { opacity: 0; }
+ 100% { opacity: 1.0; };
+}
+@-webkit-keyframes clockwait {
+ 0% { text-shadow: 0 0 20px #A6D6F2; }
+ 50% { text-shadow: 0 0 2px #A6D6F2; }
+ 100% { text-shadow: 0 0 20px #A6D6F2; }
+}
+@-moz-keyframes clockwait {
+ 0% { text-shadow: 0 0 20px #A6D6F2; }
+ 50% { text-shadow: 0 0 2px #A6D6F2; }
+ 100% { text-shadow: 0 0 20px #A6D6F2; }
+}
+keyframes clockwait {
+ 0% { text-shadow: 0 0 20px #A6D6F2; }
+ 50% { text-shadow: 0 0 2px #A6D6F2; }
+ 100% { text-shadow: 0 0 20px #A6D6F2; }
+}
+
+samp.cksum {
+ overflow-x: hidden;
+ text-overflow: ellipsis;
+ max-width: 16vw;
+ display: inline-block;
+ vertical-align: middle;
+ word-wrap: normal;
+}
+
+h1 small.authors {
+ float: right;
+ font-style: italic;
+ font-size: 42%;
+}
+.lead small.authors {
+ color: #7a8288;
+ font-style: italic;
+}
+
+a.badge:hover {
+ text-decoration: none;
+}
+.teamname {
+ -webkit-filter: invert(100%);
+ filter: invert(100%);
+}
+a:hover .teamname {
+ text-shadow: 0px 0px 10px #888888;
+}
+
+.authors a {
+ color: #3A3F44;
+}
+
+.heading {
+ font-style: italic;
+ margin-top: -7px;
+ text-align: right;
+}
+
+#eventsList {
+ overflow:hidden;
+ max-height: 90vh;
+}
+
+.swap-animation .alert {
+ margin-bottom: 0px;
+}
+.swap-animation {
+ margin-bottom: 0.5rem;
+ max-height: 30vh;
+ transition: max-height 1.0s linear,opacity 1.0s linear,transform 0.5s linear;
+}
+.swap-animation.ng-enter {
+ transform: translateY(-25vh);
+ max-height: 0vh;
+}
+.swap-animation.ng-enter-active {
+ opacity: 1;
+ transform: translateY(0px);
+ max-height: 30vh;
+}
+.swap-animation.ng-leave {
+ opacity: 1;
+ max-height: 30vh;
+ transform: translateY(0px);
+}
+.swap-animation.ng-leave-active {
+ opacity: 0;
+ transform: translateX(120vw);
+ max-height: 0vh;
+}
+
+.carousel-indicators {
+ bottom: -10px;
+}
+.carousel-caption {
+ padding: 0;
+ position: static;
+}
+.carousel .table {
+ margin-bottom: 0;
+}
+.carousel .table-sm td {
+ padding: 2px;
+}
+
+.table th.frotated {
+ border: 0;
+}
+.table th.rotated {
+ height: 100px;
+ width: 40px;
+ min-width: 40px;
+ max-width: 40px;
+ position: relative;
+ vertical-align: bottom;
+ padding: 0;
+ font-size: 12px;
+ line-height: 0.9;
+ border: 0;
+}
+
+th.rotated > div {
+ position: relative;
+ top: 0px;
+ left: -50px;
+ height: 100%;
+ transform: skew(45deg,0deg);
+ overflow: hidden;
+ border: 1px solid #000;
+}
+th.rotated div span {
+ transform: skew(-45deg,0deg) rotate(45deg);
+ position: absolute;
+ bottom: 40px;
+ left: -35px;
+ display: inline-block;
+ width: 110px;
+ text-align: left;
+ text-overflow: ellipsis;
+}
+
+ul.list-inline li {
+ display: inline;
+}
+ul.list-inline li:not(:last-child)::after {
+ content: " ● "
+}
+
+.breadcrumb-item + .breadcrumb-item::before {
+ content: ">"
+}
+
+.excard {
+ transition: transform 250ms;
+}
+.excard:hover {
+ transform: scale(1.07);
+}
+
+#tagsMenu + .dropdown-menu div {
+ overflow-y: auto;
+ max-height: calc(66vh - 100px);
+}
+
+blockquote {
+ border-left: solid 2px;
+ margin-left: 1em;
+ padding-left: 1em;
+}
+
+.jumbotron img {
+ margin-left: -1em;
+ padding-left: 2em;
+ padding-right: 2em;
+}
+img {
+ max-width: 100%;
+}
+
+#eventsList .card {
+ border-left-color: rgba(0,0,0,.125) !important;
+ border-right-color: rgba(0,0,0,.125) !important;
+ border-top-color: rgba(0,0,0,.125) !important;
+}
+
+.bg-public .card-body {
+ padding:1rem;
+ padding-bottom:0;
+}
+
+#themesSummary .card-body {
+ padding:0;
+}
+#themesSummary h3 {
+ background: rgba(64,64,64,0.66);
+ border-radius: 2px;
+ padding: 0.5rem;
+ margin-left: 0.5rem;
+ margin-right: 0.5rem;
+ margin-top: -40px;
+}
+#themesSummary p {
+ font-size: 90%;
+ margin: 0.2rem;
+ text-indent: 0.6em;
+}
+
+.card-sm .card-header, .card-sm .card-footer {
+ padding: 0.2rem 0.75rem;
+}
+.card-sm .card-body {
+ padding: 0.4rem 0.75rem;
+}
+.card-sm .card-body.text-indent p {
+ text-indent: 0.4rem;
+}
+
+.carousel-item, .carousel-caption {
+ height: inherit;
+}
+
+.page-header {
+ background-size: cover;
+ background-position: center;
+ margin-bottom: -15rem;
+}
+.page-header h1 {
+ text-shadow: 0 0 15px rgba(255,255,255,0.95), 0 0 5px rgb(255,255,255)
+}
+.page-header h1, .page-header h1 a {
+ color: black;
+}
+.page-header h1 a:hover {
+ text-decoration: none;
+}
+.page-header h2 {
+ font-size: 100%;
+ text-shadow: 1px 1px 1px rgba(0,0,0,0.9)
+}
+.page-header h2, .page-header h2 a {
+ color: #4eaee6;
+}
+.page-header h1 {
+ padding-top: 4rem;
+ text-align: center;
+}
+.page-header h2 {
+ padding-bottom: 14rem;
+ text-align: center;
+}
+
+.page-header .headerfade {
+ background: linear-gradient(transparent 0%, rgb(233,236,239) 100%);
+ height: 3rem;
+}
+
+a.list-group-item:hover {
+ text-decoration: none;
+}
diff --git a/dashboard/static/css/glyphicon.css b/dashboard/static/css/glyphicon.css
new file mode 120000
index 00000000..14cd8c56
--- /dev/null
+++ b/dashboard/static/css/glyphicon.css
@@ -0,0 +1 @@
+../../../admin/static/css/glyphicon.css
\ No newline at end of file
diff --git a/dashboard/static/fonts b/dashboard/static/fonts
new file mode 120000
index 00000000..0ef2f8d8
--- /dev/null
+++ b/dashboard/static/fonts
@@ -0,0 +1 @@
+../../admin/static/fonts/
\ No newline at end of file
diff --git a/dashboard/static/img/srs.png b/dashboard/static/img/srs.png
new file mode 100644
index 00000000..c25b6af4
Binary files /dev/null and b/dashboard/static/img/srs.png differ
diff --git a/dashboard/static/index.html b/dashboard/static/index.html
new file mode 100644
index 00000000..99afa26f
--- /dev/null
+++ b/dashboard/static/index.html
@@ -0,0 +1,640 @@
+
+
+
+
+
Tableau de bord du challenge forensic
+
+
+
+
+
+
+
+
+
+
+
+
+
+