text:
#!/usr/bin/env ruby
## Distributed under the CC0 public domain license.
## By Alison Watson. Attribution is encouraged, though not required.
## <https://creativecommons.org/publicdomain/zero/1.0/legalcode>
## ZSClassTree: Generates a class tree from ZScript files.
## Example usage:
## tools/zsclasstree.rb \
## '/mnt/g/Archive/gzdoom-g4.1.0/wadsrc/static/zscript/**/*.zs' \
## > trees/tree_g4.1.0.txt

require 'set'

class ClassSiphon
	attr_reader :structs, :classes, :clstree, :strtree, :caps

	def initialize dirs
		@subcls  = {}
		@classes = Set[]
		@structs = Set[]
		@caps    = {}

		Dir.glob dirs do |f| proc_file File.read f end

		@clstree = make_tree "OBJECT", @subcls["OBJECT"]
		@strtree = @structs.map {|type| [type, nil]}

		@classes.freeze
		@structs.freeze
		@clstree.freeze
		@strtree.freeze
		@caps.freeze
	end

	def print_classes out, filter = []
		print_tree out, filter, "OBJECT", clstree
	end

	def print_structs out, filter = []
		print_tree out, filter, "STRUCT", strtree
	end

	private
	def add_class m
		ctype =  m[0]             .strip
		cbase = (m[1] or "Object").strip
		type = ctype.upcase
		base = cbase.upcase
		@caps[type] = ctype
		@caps[base] = cbase

		if base != type
			if @subcls[base]
				@subcls[base] << type
			else
				@subcls[base] = [type]
			end
			raise "duplicate class #{type}" if @classes === type
			@classes << type
		end
	end

	def add_struct m
		ctype = m[0]
		type = ctype.upcase
		@caps[type] = ctype
		@structs << type
	end

	def proc_file f
		f.gsub! /\/\*(?!\*\/)+\*\//m, ""
		f.gsub! /\/\/.+/, ""
		f.scan(/^class\s*(\w*)[\s\w]+(?::\s+(\w*))?/).each {|m| add_class  m}
		f.scan(/^struct\s*(\w*)/)                    .each {|m| add_struct m}
	end

	# work our way down the subclasses recursively, making them into a tree structure
	def make_tree bcl, subs
		return nil unless subs
		node = {}
		subs.each do |cl| node[cl] = make_tree cl, @subcls[cl] end
		node
	end

	def print_tree out, filter, base, tree, tab = ""
		return if !tree or filter.include? base

		tree = tree.map do |type, base| [type, base] end
		tree.sort!

		for type, base in tree
			last = type == tree[-1][0]
			sep = last ? "└ " : "├ "
			nxt = last ? "  " : "│ "

			out.puts tab + sep + @caps[type]

			print_tree out, filter, type, base, tab + nxt
		end
	end
end

if __FILE__ == $0
	in_dirs = ARGV.shift
	filters = ARGV

	si = ClassSiphon.new in_dirs
	puts "Object"; si.print_classes STDOUT, filters
	puts "Struct"; si.print_structs STDOUT, filters
end

## EOF


text:
#!/bin/sh

set -e -u

case $(pwd) in
 /usr/src/app) ;;
 *) echo "$0: this should only be run from the Dockerfile inside the image" >&2
    exit 33;;
esac

if [ -d import ]; then
    for redis in import/*.rdb.gz; do
        if [ -e "$redis" ]; then
            echo "$0: importing $redis" >&2
            gzip -dc "$redis" >dump.rdb
            chmod 644 dump.rdb
            # chown redis:redis dump.rdb
            rm -f "$redis"
        else
            echo "$0: No files matching $redis" >&2
            exit 124
        fi
    done
fi

./services

mysql -u root -ppassword <<\____

    # Create the metasmoke database and metasmoke_test database

    CREATE DATABASE metasmoke;
    CREATE DATABASE metasmoke_test;
    CREATE DATABASE metasmoke_production;

    # Create the metasmoke user and password.
    # This user will only be able to access MySQL locally.
    # You should still select a strong password.
    # Grant this user privileges on the metasmoke database

    CREATE USER 'metasmoke'@'localhost' IDENTIFIED BY 'password';
    GRANT ALL PRIVILEGES ON `metasmoke`.* TO `metasmoke`@`localhost`;
    GRANT ALL PRIVILEGES ON `metasmoke_test`.* TO `metasmoke`@`localhost`;
    GRANT ALL PRIVILEGES ON `metasmoke_production`.* TO `metasmoke`@`localhost`;
    FLUSH PRIVILEGES;
____

mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root mysql

sed '/^development:/!b;x;N;N;x;a\
  adapter: mysql2\
  database: metasmoke\
  encoding: utf8\
  username: metasmoke\
  password: password\
  host: 127.0.0.1\
  port: 3306
q' config/database.sample.yml >config/database.yml

sed 's/localhost/127.0.0.1/' config/config.sample.yml >config/config.yml
sed 's/localhost/127.0.0.1/' config/cable.sample.yml >config/cable.yml

rails db:create
rails db:schema:load
rails db:migrate
# rails db:seed

enableregistration=false
localmetasmoke=false

if [ -d import ]; then
    if [ -e import/metasmoke@localhost ]; then
        localmetasmoke=true
    fi
    for sql in import/*.sql.gz; do
        if [ -e "$sql" ]; then
            echo "$0: importing $sql (this takes a long time)" >&2
            ######## FIXME: yet another hard-coded, easily guessable password
            gzip -dc <"$sql" |
            sed 's/dump_metasmoke/metasmoke/g' |
            mysql -u root -ppassword metasmoke
            rm -f "$sql"
        else
            echo "$0: No files matching $sql" >&2
            exit 123
        fi
        break
    done
else
    enableregistration=true
    localmetasmoke=true
fi

if [ "$localmetasmoke" = true ]; then
    echo "$0: creating metasmoke@localhost user" >&2
    ######## FIXME: yet another hard-coded, easily guessable password
    rails c <<\____
u = User.create(
  username: 'metasmoke', email: 'metasmoke@localhost', password: 'password')
u.add_role(:admin)
u.add_role(:developer)
u.add_role(:reviewer)
u.add_role(:flagger)
u.add_role(:core)
a = APIKey.create(
  user: u, key: "00000000000000000000000000000000", app_name: "test")
____
fi

if [ "$enableregistration" = true ]; then
    echo "$0: setting registration_enabled" >&2
    rails c <<\____
SiteSetting.create(name: 'registration_enabled', value: 1)
____
fi

repo_name: Charcoal-SE/metasmoke

repo_language: Ruby

mime_type: text/plain


text:
machine:
  ruby:
    version: ruby-2.4.1
  node:
    version: node # latest stable
dependencies:
  post:
    - curl -o- -L https://yarnpkg.com/install.sh | bash # install latest Yarn
checkout:
  post:
    - cp config/config.sample.yml config/config.yml
database:
  post:
    - bundle exec rake test:prepare
    - bundle exec rake db:migrate
    - mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root mysql
test:
  post:
    - bundle exec rubocop -D
general:
  artifacts:
    - "coverage"


text:
== README

This README would normally document whatever steps are necessary to get the
application up and running.

Things you may want to cover:

* Ruby version

* System dependencies

* Configuration

* Database creation

* Database initialization

* How to run the test suite

* Services (job queues, cache servers, search engines, etc.)

* Deployment instructions

* ...

Please feel free to use a different markup language if you do not plan to run
<tt>rake doc:app</tt>.


text:
# frozen_string_literal: true

# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.

require File.expand_path('../config/application', __FILE__)

Rails.application.load_tasks


text:
#!/bin/sh

set -e -u

service mariadb start

# Already handled by Procfile, but needed during bootstrapping
case "${1-}" in
 --no-redis) ;;
 *)
     ./redis-git/src/redis-server --port 6378 --loadmodule ./zhregex/module.so --dbfilename dump.rdb &
     ;;
esac


text:
# frozen_string_literal: true

source 'https://rubygems.org'

git_source(:github) do |repo_name|
  repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include? '/'
  "https://github.com/#{repo_name}.git"
end

gem 'awesome_print'

gem 'aescrypt', '~> 2.0', '>= 2.0.2', github: 'Charcoal-SE/aescrypt'
gem 'audited', '~> 4.4'
gem 'aws-sdk-s3'
gem 'commonmarker'
gem 'devise', '~> 4.7', '>= 4.7.2'
gem 'htmlentities', '~> 4.3', '>= 4.3.4'
gem 'httparty'
gem 'jwt'
gem 'loofah', '~> 2.19.1'
gem 'mysql2'
gem 'octokit', '~> 4.0'
gem 'paperclip', '~> 6.0.0'
gem 'puma'
gem 'rack-cors', '~> 0.4', require: 'rack/cors'
gem 'redis', '~> 3.0'
gem 'rolify'
gem 'rotp', '~> 4.0'
gem 'rqrcode'
gem 'sensible_routes', '~> 0.1.2'
gem 'travis'
gem 'webpacker', '~> 2.0'
gem 'whenever', require: false
gem 'will_paginate'
gem 'will_paginate-bootstrap', github: 'Charcoal-SE/will_paginate-bootstrap'

gem 'flamegraph'
gem 'rack-mini-profiler', '~> 1.1'
gem 'stackprof'

gem 'chartkick'
gem 'groupdate', '~> 5.0.0', require: true

gem 'rubocop', '~> 0.48.1', require: false, group: :test
gem 'simplecov', require: false, group: :test

gem 'blazer', '2.2.1.charcoal', github: 'Charcoal-SE/blazer'
gem 'grape', '1.3.0'

# gem 'puma_worker_killer'

# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rack', '>= 2.0.6'
gem 'rails', '~> 5.2.0'
# Use sqlite3 as the database for Active Record
gem 'sqlite3'
# Use SCSS for stylesheets
gem 'sass-rails'
# Use Uglifier as compressor for JavaScript assets
gem 'uglifier', '>= 1.3.0'
# Use CoffeeScript for .coffee assets and views
gem 'coffee-rails', '~> 4.2.0'
# See https://github.com/rails/execjs#readme for more supported runtimes
# gem 'therubyracer', platforms: :ruby

# Use jquery as the JavaScript library
gem 'jquery-rails'
# Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks
gem 'turbolinks'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.0'

# Use ActiveModel has_secure_password
# gem 'bcrypt', '~> 3.1.7'

# Use Unicorn as the app server
# gem 'unicorn'

gem 'capistrano'
gem 'capistrano-bundler'
gem 'capistrano-faster-assets', github: 'Charcoal-SE/capistrano-faster-assets'
gem 'capistrano-locally', require: false
gem 'capistrano-passenger'
gem 'capistrano-rails'
gem 'capistrano-rvm'
gem 'capistrano-secrets-yml'
gem 'capistrano-upload-config'

group :development do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug'

  # Access an IRB console on exception pages or by using <%= console %> in views
  gem 'web-console', '~> 3.0'

  # Spring speeds up development by keeping your application running
  # in the background. Read more: https://github.com/rails/spring
  gem 'spring'

  # Run `foreman start` to launch a local dev instance;
  # runs both `rails s` and starts the webpack server
  # https://chat.stackexchange.com/transcript/message/38020561#38020561

  gem 'foreman'

  # Export fixtures from local database
  gem 'db_fixtures_dump'

  # for the RailsPanel Chrome extension
  # https://chrome.google.com/webstore/detail/railspanel/gjpfobpafnhjhbajcjgccbbdofdckggg
  gem 'meta_request'
end

group :test do
  gem 'rails-controller-testing'
  gem 'webmock'
end

gem 'graphiql-rails'
gem 'graphql', '= 1.12.16'


text:
# frozen_string_literal: true

# This file is used by Rack-based servers to start the application.

require ::File.expand_path('../config/environment', __FILE__)
run Rails.application


text:
# Security Policy

Please see [charcoal-se.org/security](https://charcoal-se.org/security) for details of Charcoal's security and responsible
disclosure policies.


text:
web: bundle exec rails s
webpacker: ./bin/webpack-dev-server
redis: ./redis-git/src/redis-server --port 6378 --loadmodule ./zhregex/module.so --dbfilename dump.rdb


text:
#!/bin/sh

set -e -u

while true; do
    ./services --no-redis
    foreman start
    # rails server -b 0.0.0.0
    wait $(mysqld)
done


text:
FROM ruby:2.7

# To import a recent dump from metasmoke, copy the downloaded files
# to a directory named import in the current directory before building.
# Caution: The import took me over ten minutes on a MacBook Pro 2018

######## FIXME: hardcoded password "password" everywhere

# The base image ruby:2.7 is Debian Bullseye

RUN apt-get update
# allow mariadb server to start, see comment in policy-rd.d
RUN sed -i~ 's/^exit 101/exit 0/' /usr/sbin/policy-rc.d
RUN DEBIAN_FRONTEND=noninteractive \
    apt-get install -y mariadb-server mariadb-client \
       nodejs yarnpkg libpcre3-dev tzdata

# Debian stupidly reserves yarn for a different package
# https://bugs.debian.org/940511
RUN ln -fs /usr/bin/yarnpkg /usr/local/bin/yarn

# Make sure IPv6 is disabled inside Docker to avoid
# https://github.com/puma/puma/issues/1062
RUN sysctl net.ipv6.conf.all.disable_ipv6
RUN sysctl net.ipv6.conf.default.disable_ipv6

RUN rm -rf /var/lib/apt/lists/*

WORKDIR /usr/src/app

RUN git clone https://github.com/antirez/redis redis-git
RUN cd redis-git \
    && make \
    && : '# you need tcl 8.5 or newer in order to run make test' \
    && make install

RUN git clone https://github.com/Charcoal-SE/redis-zhregex zhregex
RUN cd zhregex \
    && : '# workaround for https://github.com/Charcoal-SE/redis-zhregex/issues/1' \
    && sed -i~ '/^CC=clang/d' Makefile \
    && make CC=gcc

COPY . .

ENV RUBYOPT="-KU -E utf-8:utf-8"
ENV PATH="${PATH}:/redis-git/src/redis-cli:/redis-git/src/redis-server"
RUN gem install bundler
RUN bundle update --bundler
RUN bundle install
RUN ./createdb
RUN sed -i~ 's/^web:.*/& -b 0.0.0.0/' Procfile
RUN yarn install

# Don't gripe about connections from outside localhost
# https://stackoverflow.com/a/31273925
RUN sed -i~ '/^end/i\  config.web_console.whiny_requests = false' \
    config/environments/development.rb

EXPOSE 5000 8080
CMD ["./rundb"]

# Reminder to self:
# docker build -t metasmoke .  # --progress plain
# docker run --rm -it -p5000:5000 -p8080:8080 --name metasmoke metasmoke:latest
# docker tag metasmoke:latest tripleee/metasmoke:latest
# docker push tripleee/metasmoke:latest


text:
# metasmoke

metasmoke is a web dashboard for [SmokeDetector](//github.com/Charcoal-SE/SmokeDetector), a bot that detects spam
on the [Stack Exchange network](//stackexchange.com/sites).

## API
If you're looking to develop using the metasmoke API, there's documentation available
[in the wiki](//github.com/Charcoal-SE/metasmoke/wiki/API-Documentation). You'll need an API key to access any
of the routes; ping a [metasmoke admin](//charcoal-se.org/people#admins) in
[Charcoal HQ](//chat.stackexchange.com/rooms/11540/charcoal-hq).

## WSL
While metasmoke isn't targeted to support WSL (it's just not fast enough for production use), it should work adequately
well for development purposes. The minimum Windows build version required is 16170, due to an
[lack of WSL support for mdns](https://github.com/Microsoft/WSL/issues/2245#issuecomment-310546134) in builds prior
to this.

## Docker
There is a simple `Dockerfile` here which is however not well tested.

If you want to include a database dump, create a directory `import`
and place the dump files there (one `*.rdb.gz` and one `*.sql.gz`).
This will noticeably slow down the build (plan 10-15 minutes,
depending also on disk speed and hardware).

If the `import` directory contains a file named `metasmoke@localhost`,
or if there is no `import` directory, the Docker image will create
a metasmoke user with that email address and a default password.

To create a local build, simply

    docker build -t metasmoke .

To run the image, you need to expose the ports properly.

    docker run --rm -it -p5000:5000 -p8080:8080 metasmoke

Once the image runs the initalizations, you should be able to connect to
http://localhost:5000/ and see metasmoke greet you.

Some of the options in this brief introduction are optional convenience.
If you understand what you are doing, the `-t metasmoke` is not crucial,
and the `--rm -it` options are just one common way of keeping things sane.

## License
Metasmoke is a pretty niche project, and we don't expect many people to make use of the entire thing as a whole.
However, if you want to use the code, go right ahead - metasmoke is licensed under [CC0](https://creativecommons.org/share-your-work/public-domain/cc0/). A small attribution is
appreciated, but entirely non-compulsory.

## Reporting a potential security flaw
If you wish to report a potential security flaw, no matter how minor it may be, and are not certain it's a security flaw, **only**
send the report to security@charcoal-se.org with your details about the issue you found, how you replicated it, and why you believe
it is a security flaw.  **DO NOT** disclose specific security flaws, even potential ones, via public insecure mediums such as the
public issues system or public chat systems.


text:
# Load DSL and set up stages
require 'capistrano/setup'

# Include default deployment tasks
require 'capistrano/deploy'
require 'capistrano/secrets_yml'

# Include tasks from other gems included in your Gemfile
#
# For documentation on these, see for example:
#
#   https://github.com/capistrano/rvm
#   https://github.com/capistrano/rbenv
#   https://github.com/capistrano/chruby
#   https://github.com/capistrano/bundler
#   https://github.com/capistrano/rails
#   https://github.com/capistrano/passenger
#

require 'capistrano/rvm'
# require 'capistrano/rbenv'
# require 'capistrano/chruby'
require 'capistrano/bundler'
require 'capistrano/rails/assets'
# require 'capistrano/faster_assets'
require 'capistrano/rails/migrations'
require 'capistrano/passenger'
require 'whenever/capistrano'
require 'capistrano/locally'

require 'capistrano/scm/git'
install_plugin Capistrano::SCM::Git

# Load custom tasks from `lib/capistrano/tasks` if you have any defined
Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r }


text:
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.g
# This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
# For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby

name: Test

on:
  push:
  pull_request:
  workflow_dispatch:
    inputs:
      logLevel:
        description: 'Log level'
        required: true
        default: 'warning'
      tags:
        description: 'Test scenario tags'

env:
  CIRCLE_ARTIFACTS: /tmp/circleci-artifacts
  CIRCLE_TEST_REPORTS: /tmp/circleci-test-results
  # The actions/cache holds old cache entries for 7 days and has no method of invalidating entries.
  # The only thing that can be done is to change the key value to force the cache to miss.
  # The ACTION_CACHE_VERSION is just a value to change if we don't want to use previously cached data.
  ACTION_CACHE_VERSION: cache_version_1

jobs:
  rubocop:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2
    - name: Set up Ruby
    # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
    # change this to (see https://github.com/ruby/setup-ruby#versioning):
    # uses: ruby/setup-ruby@v1
      uses: ruby/setup-ruby@v1
      with:
        ruby-version: 2.7.4
    - uses: actions/cache@v2
      with:
        path: vendor/bundle
        key: ${{ runner.os }}-gems-${{ env.ACTION_CACHE_VERSION }}-${{ hashFiles('**/Gemfile.lock') }}
        restore-keys: |
          ${{ runner.os }}-gems-${{ env.ACTION_CACHE_VERSION }}-
    - name: Install dependencies
      run: |
        sudo apt-get install libsqlite3-dev
        bundle check --path=vendor/bundle || bundle install --path=vendor/bundle --jobs=4 --retry=3
    - name: Configure
      run: |
        mkdir -p $CIRCLE_ARTIFACTS $CIRCLE_TEST_REPORTS
        cp config/config.sample.yml config/config.yml
    - run: bundle exec rubocop
  rails:

    runs-on: ubuntu-latest

    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_USER: root
          MYSQL_ROOT_PASSWORD: root
        ports:
          - 3306
      redis_main:
        image: redis
        # Map TCP port 6379 on Docker host to a random free port on the Redis container
        ports:
          - 6379/tcp
      redis_logging:
        image: redis
        ports:
          - "6379:6379"
    env:
      RAILS_ENV: test
      RACK_ENV: test
    steps:
    - uses: actions/checkout@v2
    - name: Set up Ruby
    # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
    # change this to (see https://github.com/ruby/setup-ruby#versioning):
    # uses: ruby/setup-ruby@v1
      uses: ruby/setup-ruby@v1
      with:
        ruby-version: 2.7.4
    - name: Cache bundle
      uses: actions/cache@v2
      with:
        path: vendor/bundle
        key: ${{ runner.os }}-gems-${{ env.ACTION_CACHE_VERSION }}-${{ hashFiles('**/Gemfile.lock') }}
        restore-keys: |
          ${{ runner.os }}-gems-${{ env.ACTION_CACHE_VERSION }}-
    - name: Cache yarn packages
      uses: actions/cache@v2
      with:
        path: node_modules
        key: ${{ runner.os }}-node-modules-yarn-${{ env.ACTION_CACHE_VERSION }}-${{ hashFiles('**/Gemfile.lock') }}
        restore-keys: |
          ${{ runner.os }}-node-modules-yarn-${{ env.ACTION_CACHE_VERSION }}-
    - name: Install dependencies
      run: |
        sudo apt-get install libsqlite3-dev
        bundle check --path=vendor/bundle || bundle install --path=vendor/bundle --jobs=4 --retry=3
        yarn install
    - name: Start MySQL
      run: |
        sudo systemctl start mysql.service
        mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root --password=root mysql
    - name: Configure
      run: |
        mkdir -p $CIRCLE_ARTIFACTS $CIRCLE_TEST_REPORTS
        cp config/config.sample.yml config/config.yml
        mkdir -p config && echo 'test:
          adapter: mysql2
          database: metasmoke_test
          username: root
          password: root
          host: localhost
          port: ${{ job.services.mysql.ports['3306'] }}
        ' > config/database.yml
        mkdir -p config && echo 'test:
          adapter: redis
          timeout: 120
          url: redis://localhost:${{ job.services.redis_main.ports['6379'] }}
        ' > config/cable.yml
    - name: Prepare
      run: bundle exec rake db:create db:schema:load db:test:prepare db:migrate --trace
    - name: Run tests
      run: bundle exec rails test --verbose
    - name: Upload Gemfile.lock
      uses: actions/upload-artifact@v2
      if: ${{ failure() }}
      with:
        name: Gemfile.lock
        path: Gemfile.lock
    - name: Gem version diff
      if: ${{ failure() }}
      run: git diff Gemfile.lock
    - name: Upload coverage data
      uses: actions/upload-artifact@v2
      if: ${{ success() }}
      with:
        name: coverage
        path: coverage
  js:
    runs-on: ubuntu-latest
    env:
      NODE_ENV: test
      RAILS_ENV: test
      RACK_ENV: test
    steps:
      - uses: actions/checkout@v2
      - run: export PATH="~/Charcoal-SE/metasmoke/node_modules/.bin:$PATH"
      - uses: actions/cache@v2
        with:
          path: node_modules
          key: ${{ runner.os }}-node-modules-npm-${{ env.ACTION_CACHE_VERSION }}-${{ hashFiles('**/Gemfile.lock') }}
          restore-keys: |
            ${{ runner.os }}-node-modules-npm-${{ env.ACTION_CACHE_VERSION }}-
      - run: ls
      - name: Install dependencies
        run: yarn install
      - name: Run tests
        run: npm test


text:
#!/bin/bash

set -e

METASMOKE_ROOT=/var/railsapps/metasmoke
timestamp=$(date +%s)
filename=dump_metasmoke_clean-$timestamp.sql.gz
username=$1
password=$2
host=$3

echo "CREATE DATABASE dump_metasmoke" | mysql -h $host -u $username --password=$password
mysqldump -h $host -u $username --password=$password metasmoke | mysql -h $host -u $username --password=$password dump_metasmoke;
mysql -h $host -u $username --password=$password dump_metasmoke < $METASMOKE_ROOT/current/dump/redact.sql
mysqldump -h $host -u $username --password=$password dump_metasmoke | gzip > $METASMOKE_ROOT/shared/dumps/$filename
echo "DROP DATABASE dump_metasmoke;" | mysql -h $host -u $username --password=$password

# Remove old dumps
ls -d $METASMOKE_ROOT/shared/dumps/* | grep -v $filename | xargs rm


text:
# Necessary for TRUNCATE calls to work properly.
SET FOREIGN_KEY_CHECKS=0;

DROP TABLE `ar_internal_metadata`;
TRUNCATE TABLE `api_tokens`;
UPDATE `api_keys` SET `key` = '';
UPDATE `audits` SET `remote_address` = '';
TRUNCATE TABLE `emails_preferences`;
TRUNCATE TABLE `emails_addressees`;
TRUNCATE TABLE `flags`;
UPDATE `smoke_detectors` SET `access_token` = '';
UPDATE `users` SET
    `email` = CONCAT(`id`, '@metasmoke.fake'),
    `encrypted_password` = '',
    `reset_password_token` = '',
    `reset_password_sent_at` = NULL,
    `encrypted_api_token_legacy` = '',
    `two_factor_token` = NULL,
    `enabled_2fa` = NULL,
    `salt` = NULL,
    `iv` = NULL;


text:
# Moderator Sites

---
/api/v2.0/mods | List all ModeratorSites.

Available tables: `moderator_sites`.
Output format:

    {
        "items": [
            {
                "id": 970,
                "user_id": 5,
                "site_id": 1
            }
        ],
        "has_more": true
    }
---
/api/v2.0/mods/user/:id | List all ModeratorSites for the specified user.

Available tables: `moderator_sites`.
Output format: identical to `/mods`.


text:
# Domains

---
/api/v2.0/domains | List all domains.

Available tables: `spam_domains`.
Output format:

    {
        "items": [
            {
                "id": 12935,
                "domain": "infinitemlmsoftware.com",
                "whois": null
            }
        ],
        "has_more": true
    }
---
/api/v2.0/domains/name/:name | Find a domain record by its domain name

Available tables: `spam_domains`.
Output format: identical to `/domains`.
---
/api/v2.0/domains/:id/posts | List all posts containing a specific domain.

Available tables: `posts`.
Output format:

    {
        "items": [
            {
                "id": 77988,
                "title": "Google Webmaster Tools show the error in Manual Action for my web - https://infinitemlmsoftware.com/",
                "link": "//drupal.stackexchange.com/questions/242498",
                "site_id": 86,
                "user_link": "https://drupal.stackexchange.com/users/77863/isha",
                "username": "isha",
                "user_reputation": 1
            }
        ],
        "has_more": false
    }
---
/api/v2.0/domains/:id/tags/add | [W] Add a tag to a specified domain.

**Write route**. Requires a POST request, valid write token, and params: `tag` (the name of the tag to add).
Available tables: `domain_tags`.
Output format:

    {
        "items": [
            {
                "id": 16,
                "name": "blargle",
                "description": null
            }
        ],
        "has_more": true
    }


text:
# Posts

---
/api/v2.0/posts | List all posts.

Available tables: `posts`.
Output format:

    {
        "items": [
            {
                "id": 78099,
                "title": "test",
                "link": "//stackoverflow.com/a/3498236",
                "site_id": 1,
                "user_link": "//stackoverflow.com/u/3160466",
                "username": "ArtOfCode",
                "user_reputation": 3478
            }
        ],
        "has_more": true
    }
---
/api/v2.0/posts/between | List posts within a specified date range.

Requires params: `from` (range start date) and `to` (range end date). Both are in ISO 8601 format - `1970-01-01T00:00:00.000000Z`.
Available tables: `posts`.
Output format: identical to `/posts`.
---
/api/v2.0/posts/site | List posts on a specific site.

Requires params: `site` (site domain name).
Available tables: `posts`.
Output format: identical to `/posts`.
---
/api/v2.0/posts/urls | List posts specified by their URLs.

Requires params: `urls` (a **comma** separated list of URLs).
URLs are of the format //stackoverflow.com/a/12345 (for answers) or //stackoverflow.com/questions/12345 (for questions).
Available tables: `posts`.
Output format: identical to `/posts`.
---
/api/v2.0/posts/feedback | List posts by feedback types.

Requires params: `type` (feedback type).
Available tables: `posts`.
Output format: identical to `/posts`.
---
/api/v2.0/posts/undeleted | List posts that have not yet been deleted.

Looks for the absence of a DeletionLog record with `is_deleted: true` on the post - may not be 100% accurate; use the SE API if that's what you need.
Available tables: `posts`.
Output format: identical to `/posts`.
---
/api/v2.0/posts/uid/:api_param/:native_id | Get a post by its API site parameter and SE-native ID.

Finds posts on the site specified by `:api_param` with the post ID specified in `:native_id`, and returns them.
Available tables: `posts`.
Output format: identical to `/posts`.
---
/api/v2.0/posts/:ids | List posts by their IDs

`ids` is a **comma** separated list of post IDs to return.
Available tables: `posts`.
Output format: identical to `/posts`.
---
/api/v2.0/posts/:id/reasons | List the reasons a post was caught by.

Available tables: `reasons`.
Output format:

    {
        "items": [
            {
                "id": 106,
                "reason_name": "Repeated url at end of long post",
                "weight": 99
            }
        ],
        "has_more": true
    }
---
/api/v2.0/posts/:id/domains | List the domain names contained in a post.

Available tables: `spam_domains`.
Output format:

    {
        "items": [
            {
                "id": 3993,
                "domain": "www.drhelpnutrition.org",
                "whois": null
            }
        ],
        "has_more": false
    }
---
/api/v2.0/posts/:id/flags | Get details of what automatic and manual flags have been cast on a post.

Available tables: N/A. This route provides a specific, unfilterable subset of information.
Output format:

    {
        "items": [
            {
                "id": 78000,
                "autoflagged": {
                    "flagged": true,
                    "users": [
                        {
                            "id": 14,
                            "username": "ProgramFOX",
                            "stackexchange_chat_id": 123456,
                            "stackoverflow_chat_id": 123456,
                            "meta_chat_id": 123456
                        },
                        {
                            "id": 42,
                            "username": "QPaysTaxes",
                            "stackexchange_chat_id": 123456,
                            "stackoverflow_chat_id": 123456,
                            "meta_chat_id": 123456
                        },
                        {
                            "id": 226,
                            "username": "guiniveretoo",
                            "stackexchange_chat_id": 123456,
                            "stackoverflow_chat_id": 123456,
                            "meta_chat_id": 123456
                        }
                    ]
                },
                "manual_flags": {
                    "users": [
                        {
                            "id": 7,
                            "username": "ArtOfCode",
                            "stackexchange_chat_id": 123456,
                            "stackoverflow_chat_id": 123456,
                            "meta_chat_id": 123456
                        }
                    ]
                }
            }
        ],
        "has_more": false
    }
---
/api/v2.0/posts/report | [W] Report a post.

**Write route**. Requires a POST request, valid write token, and parameters: `url` (the post URL to report).
Available tables: N/A. No database information is returned from this request.
All requests to this route will be met with a 202 Accepted status; metasmoke does not know the status of reports as it just forwards them to Smokey.
Output format:

    {
        "status": "Accepted"
    }
---
/api/v2.0/posts/:id/flag | [W] Cast a flag on a post.

**Write route**. Requires a POST request and valid write token.
Available tables: N/A. No database information is returned from this request.
`backoff` will only be set on successful requests. `message` will only be set on failed requests.
Specify the `flag_type` param to choose your flag type: either `spam` (default) or `abusive` for an R/A flag.
Output format:

    {
        "status": "success|failed",
        "backoff": "10",
        "message": "failure reason"
    }
---
/api/v2.0/posts/search{.:format} | Get an RSS or Atom feed of posts matching certain criteria.

`format` may be either `rss` or `atom`, e.g. `/api/v2.0/posts/search.rss`.
Filters are not available on this route.
Output format for RSS and Atom feeds is consistent with RSS and Atom standards, respectively.


text:
# Users

---
/api/v2.0/users | List all users.

Available tables: `users`.
Output format:

    {
        "items": [
            {
                "id": 311,
                "username": "Art"
            }
        ],
        "has_more": true
    }

---
/api/v2.0/users/with_role/:role | List all users with the specified role

Available tables: `users`.
Output format:

    {
        "items": [
            {
                "id": 311,
                "username": "Art"
            }
        ],
        "has_more": true
    }
---
/api/v2.0/users/current-roles | Get roles for the current user

Available tables: `roles`.
Output format:

    {
        "items": [
            {
                "id": 1,
                "name": "admin"
            }
        ],
        "has_more": false
    }


text:
# Deletion logs

---
/api/v2.0/deletions | List all deletion logs.

Available tables: `deletion_logs`.
Output format:

    {
        "items": [
            {
                "id": 79044,
                "post_id": 78091,
                "is_deleted": true
            }
        ],
        "has_more": true
    }
---
/api/v2.0/deletions/post/:id | List deletion logs on the post specified.

Available tables: `deletion_logs`.
Output format: identical to `/deletions`.
---
/api/v2.0/deletions/post/:id | [W] Add a deletion log to a post.

**Write route.** Requires a POST request, trusted key, and valid write token.
Available tables: N/A. All fields on the created deletion log are returned.
Parameters: `is_deleted` (Boolean, required), `uncertainty` (Integer, seconds uncertainty, optional), `timestamp` (DateTime, deletion timestamp, optional)
Output format:

    {
        "items": [
            {
                "id": 106575,
                "post_id": 100000,
                "is_deleted": true,
                "created_at": "2021-01-01T00:00:00.000Z",
                "updated_at": "2018-02-15T21:12:02.000Z",
                "api_key_id": 1,
                "uncertainty": 0
            }
        ],
        "has_more": false
    }


text:
# SmokeDetectors

---
/api/v2.0/smokeys | List all SmokeDetector instances.

Available tables: `smoke_detectors`.
Output format:

    {
        "items": [
            {
                "id": 17,
                "last_ping": "2017-07-24T00:47:50.000Z",
                "location": "The Call of Midnight",
                "user_id": 47,
                "status_color": "critical"
            }
        ],
        "has_more": true
    }


text:
# Apps

---
/api/v2.0/apps | List all apps.

Available tables: `api_keys`.
Output format:

    {
        "items": [
            {
                "id": 21,
                "app_name": "thesecretmaster's app",
                "user_id": 298,
                "github_link": ""
            }
        ],
        "has_more": true
    }


text:
# Reasons

---
/api/v2.0/reasons | List all reasons.

Available tables: `reasons`.
Output format:

    {
        "items": [
            {
                "id": 134,
                "reason_name": "Pattern-matching email in body",
                "weight": 0
            }
        ],
        "has_more": true
    }
---
/api/v2.0/reasons/:ids | List reasons specified by their IDs.

Available tables: `reasons`.
Output format: identical to `/reasons`.
---
/api/v2.0/reasons/:id/posts | List posts caught by a specific reason.

Available tables: `posts`.
Output format:

    {
        "items": [
            {
                "id": 78099,
                "title": "test",
                "link": "//stackoverflow.com/a/3498236",
                "site_id": 1,
                "user_link": "//stackoverflow.com/u/3160466",
                "username": "ArtOfCode",
                "user_reputation": 3478
            }
        ],
        "has_more": true
    }


text:
# Debug

---
/api/v2.0/debug | Return debugging information for the API.

No database queries are performed by this route; no tables are available.
Not really intended for API consumer use, more for API developers.
Output format:

    {
        "params": {
            "key": "abcdef012345",
            "per_page": "1"
        },
        "ts": "2017-12-18T00:46:14.812+00:00",
        "filters": {
            "announcements": "IJIHMOFFGGKIOKOGFJHMHHLOMIJFNGLLOHKMGIFKOMKLIILOGHLNNMJHJ",
            "apps": "MJMFOGKMLGMHGHFNGNJJOLOJGMKMIKMMKMHMFGJGIKIHHFLJKFLFLFN",
            "commits": "GFHIFKGGJLGIGLIHFJHMJHKMOINHOFGIONGGIMKOGKHMNFFNIH",
            "deletions": "MOOHKNMFMOGKIIMKIIIOHLJFGJHNOGMGMHMLIMJIINGFOJJ",
            "tags": "GHJNNJGMIGGGMMGJNOKOHLFFFHHIHLNIFNHJJIIJOFIHOL",
            "feedbacks": "HJKLKLLJLIHMKLINOKKJIJMOIOLKOMGJHGFMHGJIGKKH",
            "posts": "HKOMIKHOJJOHKLINJNOHMHMHKFGHJN",
            "reasons": "HOKGJMOFKGMOIKHNHKNKLFF",
            "smokeys": "ILMLMLLNNIHNHOJJ",
            "domains": "INJNHOFLOMHGL",
            "users": "JIKH"
        }
    }
---
/api/v2.0/debug/filter | Decode a filter back to a list of fields.

No database queries are performed by this route; no tables are available.
Pass a filter as the `filter` parameter to receive a list of fields the filter contains.
Output format:

    {
        "fields": [
            "posts.id",
            "posts.title",
            "posts.link",
            "posts.site_id",
            "posts.user_link",
            "posts.username",
            "posts.user_reputation"
        ]
    }


text:
# Feedbacks

---
/api/v2.0/feedbacks | List all feedbacks

Available tables: `feedbacks`.
Output format:

    {
        "items": [
            {
                "id": 141289,
                "feedback_type": "fp-",
                "post_id": 78000,
                "user_id": 7
            }
        ],
        "has_more": true
    }
---
/api/v2.0/feedbacks/post/:id | List all feedbacks on the post specified.

Available tables: `feedbacks`.
Output format: identical to `/feedbacks`.
---
/api/v2.0/feedbacks/user/:id | List all feedbacks by the user specified.

Available tables: `feedbacks`.
Output format: identical to `/feedbacks`.
---
/api/v2.0/feedbacks/app/:id | List all feedbacks created by the app specified.

Available tables: `feedbacks`.
Output format: identical to `/feedbacks`.
---
/api/v2.0/feedbacks/post/:id/create | [W] Create a feedback on the post specified.

Available tables: `feedbacks`. Returns all feedbacks on the post.
**Write route**. Requires POST request, valid write token, and parameters: `type` (feedback type, i.e. `tpu-`).
Output format: identical to `/feedback`.


text:
# Commit Statuses

---
/api/v2.0/commits | List all commit statuses.

Available tables: `commit_statuses`. Output format:

    {
        "items": [
            {
                "id": 3689,
                "commit_sha": "cb94d4592f0a0085302ad2dbed1b4aaa5db85d09",
                "status": "success"
            }
        ],
        "has_more": true
    }


text:
# Announcements

---
/api/v2.0/announcements | List all announcements.

Available tables: `announcements`
Output format:

    {
        "items": [
            {
                "id": 25,
                "text": "Test API announcement",
                "expiry": "2017-12-13T19:00:00.000Z"
            }
        ],
        "has_more": true
    }

---
/api/v2.0/announcements/active | List active (non-expired) announcements.

Available tables: `announcements`
Output format: identical to `/announcements`.
---
/api/v2.0/announcements/expired | List expired announcements.

Available tables: `announcements`
Output format: identical to `/announcements`.
---
/api/v2.0/announcements/create | [W] Create an announcement.

Available tables: N/A. All fields are returned. Filter is ignored.
Requires params: `text` and `expiry`. Requires core role.
Output format:

    {
        "items": [
            {
                "id": 27,
                "text": "test from API",
                "expiry": "2017-01-01T00:00:00.000Z",
                "created_at": "2017-12-17T23:11:55.000Z",
                "updated_at": "2017-12-17T23:11:55.000Z"
            }
        ],
        "has_more": false
    }


text:
# Comments

---
/api/v2.0/comments | List all comments.

Available tables: `post_comments`.
Output format:

    {
        "items": [
            {
                "id": 2,
                "user_id": 1373,
                "text": "Some more words."
            }
        ],
        "has_more": true
    }
---
/api/v2.0/comments/post/:id | List all comments on a specified post.

Available tables: `post_comments`.
Output format: identical to `/comments`.
---
/api/v2.0/comments/post/:id/add | Add a comment to a post.

Requires parameters: `text`.
Available tables: N/A. All fields on the created comment are returned.
Output format: identical to `/comments`.
---
/api/v2.0/comments/post/:id | [W] [Smokey] Add an automatic comment to a post.

**This route is for use by SmokeDetector instances only.**
Requires parameters: `text`, `chat_user_id`, and `chat_host`.
Available tables: N/A. All fields on the created comment are returned.
Output format: identical to `/comments`.


text:
# Tags

---
/api/v2.0/tags | List all tags.

Available tables: `domain_tags`.
Output format:

    {
        "items": [
            {
                "id": 16,
                "name": "blargle",
                "description": null
            }
        ],
        "has_more": true
    }
---
/api/v2.0/tags/name/:name | List tags specified by their name.

Available tables: `domain_tags`.
Output format: identical to `/tags`.
---
/api/v2.0/tags/:id/domains | List the domains that a specific tag is used on.

Available tables: `spam_domains`. Also available is `/api/v2.0/tags/name/:name/domains`, which returns domains on a tag specified by its name.
Output format:

    {
        "items": [
            {
                "id": 12935,
                "domain": "infinitemlmsoftware.com",
                "whois": null
            }
        ],
        "has_more": false
    }


text:
# Sites

---
/api/v2.0/sites | List all sites

Available tables: `sites`.
Output format:

    {
        "items": [
            {
                "id": 1,
                "site_name": "Stack Overflow",
                "site_url": "https://stackoverflow.com"
            }
        ],
        "has_more": true
    }


text:
# frozen_string_literal: true

module API
  class UsersAPI < API::Base
    get '/' do
      std_result User.all.order(id: :desc), filter: FILTERS[:users]
    end

    get '/with_role/:role' do
      std_result User.with_role(params[:role]).order(id: :desc), filter: FILTERS[:users]
    end

    before do
      authenticate_user!
    end
    params do
      requires :key, type: String
      requires :token, type: String
    end
    get '/current-roles' do
      std_result current_user.roles, filter: FILTERS[:roles]
    end
  end
end


text:
# frozen_string_literal: true

module API
  class ReasonsAPI < API::Base
    get '/' do
      std_result Reason.all.order(id: :desc), filter: FILTERS[:reasons]
    end

    get ':ids' do
      std_result Reason.where(id: params[:ids].split(',')).order(id: :desc), filter: FILTERS[:reasons]
    end

    get ':id/posts' do
      std_result Reason.find(params[:id]).posts.order(id: :desc), filter: FILTERS[:posts]
    end
  end
end


text:
# frozen_string_literal: true

module API
  class CommentsAPI < API::Base
    get '/' do
      std_result PostComment.all.order(id: :desc), filter: FILTERS[:comments]
    end

    get 'post/:id' do
      std_result PostComment.where(post_id: params[:id]).order(id: :desc), filter: FILTERS[:comments]
    end

    params do
      requires :key, type: String
      requires :token, type: String
      requires :text, type: String
    end
    post 'post/:id/add' do
      authenticate_user!

      comment = PostComment.create(post_id: params[:id], text: params[:text], user: current_user)
      single_result comment
    end

    params do
      requires :key, type: String
      requires :text, type: String
      requires :chat_user_id, type: Integer
      requires :chat_host, type: String
    end
    post 'post/:id' do
      authenticate_smokey!

      id_field = { 'stackexchange.com' => :stackexchange_chat_id, 'stackoverflow.com' => :stackoverflow_chat_id,
                   'meta.stackexchange.com' => :meta_stackexchange_chat_id }[params[:chat_host]]
      error!({ name: 'bad_parameter', detail: 'chat_host is unrecognized' }, 400) if id_field.nil?

      user = User.find_by(id_field => params[:chat_user_id]) || User.find(-1)
      comment = PostComment.create(user: user, post_id: params[:id], text: params[:text])

      single_result comment
    end
  end
end


text:
# frozen_string_literal: true

module API
  class SpamDomainsAPI < API::Base
    get '/' do
      std_result SpamDomain.all.order(id: :desc), filter: FILTERS[:domains]
    end

    get 'name/:name' do
      # Newer versions of Ruby drop everything after the first dot
      # Parse the raw request to get the actual query string
      # request.env['PATH_INFO'] is '/v2.0/domains/name/test.spam.com'
      name = request.env['PATH_INFO'].split('/domains/name/', 2)[1].split(/[?&]/, 2)[0]
      std_result SpamDomain.where(domain: name).order(id: :desc), filter: FILTERS[:domains]
    end

    get ':id/posts' do
      std_result SpamDomain.find(params[:id]).posts.order(id: :desc), filter: FILTERS[:posts]
    end

    before do
      authenticate_user!
      role :core
    end
    params do
      requires :key, type: String
      requires :token, type: String
      requires :tag, type: String
    end
    post ':id/tags/add' do
      tag = DomainTag.find_or_create_by name: params[:tag]
      domain = SpamDomain.find params[:id]
      domain.domain_tags << tag
      std_result domain.domain_tags.order(id: :desc), filter: FILTERS[:tags]
    end
  end
end


text:
# frozen_string_literal: true

module API
  class AppsAPI < API::Base
    get '/' do
      std_result APIKey.all.order(id: :desc), filter: FILTERS[:apps]
    end
  end
end


text:
# frozen_string_literal: true

module API
  class PostsAPI < API::Base
    get '/' do
      std_result Post.all.order(id: :desc), filter: FILTERS[:posts]
    end

    params do
      requires :key, type: String
      optional :site, type: String
    end
    get 'search.atom' do
      @posts = Post.all

      if params[:site].present?
        @posts = @posts.joins(:site).where(sites: { site_domain: params[:site] })
      end

      @posts = @posts.order(id: :desc)
      @posts = paginated(@posts)

      # noinspection RubyArgCount
      content_type 'application/atom+xml'
      render 'api/posts_atom.xml.erb', posts: @posts
    end

    params do
      requires :key, type: String
      optional :site, type: String
    end
    get 'search.rss' do
      @posts = Post.all

      if params[:site].present?
        @posts = @posts.joins(:site).where(sites: { site_domain: params[:site] })
      end

      @posts = @posts.order(id: :desc)
      @posts = paginated(@posts)

      # noinspection RubyArgCount
      content_type 'application/rss+xml'
      render 'api/posts_rss.xml.erb', posts: @posts
    end

    params do
      requires :key, type: String
      requires :from, type: DateTime
      requires :to, type: DateTime
    end
    get 'between' do
      std_result Post.where('created_at > ?', params[:from]).where('created_at < ?', params[:to]).order(id: :desc),
                 filter: FILTERS[:posts]
    end

    params do
      requires :key, type: String
      requires :site, type: String
    end
    get 'site' do
      std_result Post.joins(:site).where(sites: { site_domain: params[:site] }).order(id: :desc), filter: FILTERS[:posts]
    end

    params do
      requires :key, type: String
      requires :urls, type: String
    end
    get 'urls' do
      std_result Post.where(link: params[:urls].split(',')).order(id: :desc), filter: FILTERS[:posts]
    end

    params do
      requires :key, type: String
      requires :type, type: String
    end
    get 'feedback' do
      std_result Post.joins(:feedbacks).where(feedbacks: { feedback_type: params[:type] }).order(id: :desc),
                 filter: FILTERS[:posts]
    end

    get 'undeleted' do
      std_result Post.where(deleted_at: nil).order(id: :desc), filter: FILTERS[:posts]
    end

    get 'uid/:api_param/:native_id' do
      # Newer versions of Ruby drop everything after the first dot
      # Parse the raw request to get the actual query string
      # request.env['PATH_INFO'] is '/v2.0/uid/foo.bar/1234'
      tail = request.env['PATH_INFO'].split(/[?&]/, 2)[0].split('/uid/', 2)[1]
      values = tail.split('/', 2)
      std_result Post.joins(:site).where(sites: { api_parameter: values[0] }, posts: { native_id: values[1] }),
                 filter: FILTERS[:posts]
    end

    get ':ids' do
      std_result Post.where(id: params[:ids].split(',')).order(id: :desc), filter: FILTERS[:posts]
    end

    get ':id/reasons' do
      std_result Post.find(params[:id]).reasons.order(id: :desc), filter: FILTERS[:reasons]
    end

    get ':id/domains' do
      std_result Post.find(params[:id]).spam_domains.order(id: :desc), filter: FILTERS[:domains]
    end

    get ':id/flags' do
      post = Post.find params[:id]
      flag_data = {
        id: post.id,
        autoflagged: {
          flagged: post.flag_logs.auto.successful.any?,
          users: post.flag_logs.auto.successful.includes(:user).map do |u|
            user = u.user
            { id: user&.id, username: user&.username, stackexchange_chat_id: user&.stackexchange_chat_id,
              stackoverflow_chat_id: user&.stackoverflow_chat_id, meta_chat_id: user&.meta_stackexchange_chat_id }
          end
        },
        manual_flags: {
          users: post.flag_logs.manual.successful.includes(:user).map do |u|
            user = u.user
            { id: user&.id, username: user&.username, stackexchange_chat_id: user&.stackexchange_chat_id,
              stackoverflow_chat_id: user&.stackoverflow_chat_id, meta_chat_id: user&.meta_stackexchange_chat_id }
          end
        }
      }
      single_result flag_data
    end

    before do
      authenticate_user!
      role :reviewer
    end

    params do
      requires :key, type: String
      requires :token, type: String
      requires :url, type: String
    end
    post 'report' do
      ActionCable.server.broadcast 'smokedetector_messages', report: { user: current_user.username, post_link: params[:url] }
      status 202
      { status: 'Accepted' }
    end

    params do
      requires :key, type: String
      requires :token, type: String
    end
    post ':id/flag' do
      post = Post.find params[:id]

      unless current_user.write_authenticated && current_user.flags_enabled
        error!({
                 error_name: 'not_write_authenticated',
                 error_code: 409,
                 error_message: 'Current user is not write-authenticated.'
               }, 409)
      end

      flag_type = params[:flag_type].present? ? params[:flag_type] : :spam
      status, message = current_user.flag(flag_type, post, false)
      FlagLog.create(
        success: status,
        error_message: status.present? ? nil : message,
        is_dry_run: false,
        flag_condition: nil,
        user: current_user,
        post: post,
        backoff: status.present? ? message : 0,
        site_id: post.site_id,
        is_auto: false,
        api_key: @key,
        flag_type: flag_type.to_s
      )
      if status
        { status: 'success', backoff: message }
      else
        status 500
        { status: 'failed', message: message }
      end
    end
  end
end


text:
# frozen_string_literal: true

module API
  class SitesAPI < API::Base
    get '/' do
      std_result Site.all, filter: FILTERS[:sites]
    end
  end
end


text:
# frozen_string_literal: true

module API
  class AnnouncementsAPI < API::Base
    get '/' do
      std_result Announcement.all.order(id: :desc), filter: FILTERS[:announcements]
    end

    get 'active' do
      std_result Announcement.where('expiry > ?', DateTime.now).order(id: :desc), filter: FILTERS[:announcements]
    end

    get 'expired' do
      std_result Announcement.where('expiry < ?', DateTime.now).order(id: :desc), filter: FILTERS[:announcements]
    end

    before do
      authenticate_user!
      role :core
    end
    params do
      requires :key, type: String
      requires :token, type: String
      requires :text, type: String
      requires :expiry, type: DateTime
    end
    post 'create' do
      announcement = Announcement.create(text: params[:text], expiry: params[:expiry])
      single_result announcement
    end
  end
end


text:
# frozen_string_literal: true

module API
  class DomainTagsAPI < API::Base
    get '/' do
      std_result DomainTag.all.order(id: :desc), filter: FILTERS[:tags]
    end

    get 'name/:name' do
      std_result DomainTag.where(name: params[:name]).order(id: :desc), filter: FILTERS[:tags]
    end

    get 'name/:name/domains' do
      std_result DomainTag.find_by(name: params[:name])&.spam_domains || [], filter: FILTERS[:domains]
    end

    get ':id/domains' do
      std_result DomainTag.find(params[:id]).spam_domains, filter: FILTERS[:domains]
    end
  end
end


text:
# frozen_string_literal: true

module API
  class CommitStatusesAPI < API::Base
    get '/' do
      std_result CommitStatus.all.order(id: :desc), filter: FILTERS[:commits]
    end
  end
end


text:
# frozen_string_literal: true

module API
  class FeedbacksAPI < API::Base
    get '/' do
      std_result Feedback.all.order(id: :desc), filter: FILTERS[:feedbacks]
    end

    get 'post/:id' do
      std_result Feedback.where(post_id: params[:id]).order(id: :desc), filter: FILTERS[:feedbacks]
    end

    get 'user/:id' do
      std_result Feedback.where(user_id: params[:id]).order(id: :desc), filter: FILTERS[:feedbacks]
    end

    get 'app/:id' do
      std_result Feedback.where(api_key_id: params[:id]).order(id: :desc), filter: FILTERS[:feedbacks]
    end

    before do
      authenticate_user!
      role :reviewer
    end
    params do
      requires :key, type: String
      requires :token, type: String
      requires :type, type: String
    end
    post 'post/:id/create' do
      post = Post.find params[:id]
      feedback = Feedback.new user: current_user, post: post, api_key: @key, feedback_type: params[:type]

      if post.question? && feedback.is_naa?
        error!({ name: 'illegal', detail: 'NAA feedback is not allowed on questions.' }, 400)
      end

      if feedback.save
        if feedback.is_naa?
          begin
            ActionCable.server.broadcast 'smokedetector_messages', naa: { post_link: @post.link }
          rescue # rubocop:disable Lint/HandleExceptions
          end
        elsif feedback.is_negative?
          begin
            ActionCable.server.broadcast 'smokedetector_messages', fp: { post_link: @post.link }
          rescue # rubocop:disable Lint/HandleExceptions
          end
        end

        std_result post.feedbacks.order(id: :desc), filter: FILTERS[:feedbacks]
      else
        error!({ name: 'persistence_fail', detail: 'Feedback object failed to save.' }, 500)
      end
    end
  end
end


text:
# frozen_string_literal: true

module API
  class SmokeDetectorsAPI < API::Base
    get '/' do
      std_result SmokeDetector.all.order(id: :desc), filter: FILTERS[:smokeys]
    end
  end
end


text:
# frozen_string_literal: true

module API
  class Base < Grape::API
    def self.filter(fields)
      Filterator::V2.filter_from_fields(fields)
    end

    FILTERS = {
      announcements: filter(Announcement.fields(:id, :text, :expiry)),
      apps: filter(APIKey.fields(:id, :app_name, :user_id, :github_link)),
      commits: filter(CommitStatus.fields(:id, :commit_sha, :status)),
      deletions: filter(DeletionLog.fields(:id, :post_id, :is_deleted)),
      tags: filter(DomainTag.fields(:id, :name, :description)),
      feedbacks: filter(Feedback.fields(:id, :feedback_type, :post_id, :user_id)),
      posts: filter(Post.fields(:id, :title, :link, :site_id, :user_link, :username, :user_reputation)),
      reasons: filter(Reason.fields(:id, :reason_name, :weight)),
      roles: filter(Role.fields(:id, :name)),
      smokeys: filter(SmokeDetector.fields(:id, :last_ping, :location, :user_id)),
      domains: filter(SpamDomain.fields(:id, :domain, :whois)),
      users: filter(User.fields(:id, :username)),
      mods: filter(ModeratorSite.fields(:id, :user_id, :site_id)),
      sites: filter(Site.fields(:id, :site_name, :site_url)),
      comments: filter(PostComment.fields(:id, :user_id, :text))
    }.freeze

    format :json

    helpers do
      def current_user
        @current_user ||= @token.user
      end

      def authenticate_app!
        @key = APIKey.find_by(key: params[:key])
        if @key.nil? && SmokeDetector.find_by(access_token: params[:key])
          @key = APIKey.first
        end
        error!({ name: 'missing_key', detail: 'No key was provided or the provided key is invalid.' }, 403) if @key.blank?
      end

      def authenticate_user!
        if @key.key_type == 'read_only'
          error!({ name: 'read_only_key', detail: 'The key provided is not authorized for write operations.' }, 401)
          return
        end
        @token = @key.api_tokens.find_by token: params[:token]
        error!({ name: 'missing_token', detail: 'No token was provided or the provided token is invalid.' }, 401) if @token.blank?
      end

      def authenticate_smokey!
        return if SmokeDetector.find_by(access_token: params[:key]).present?
        error!({ name: 'insufficient_authorization', detail: 'The requested action requires a higher level of authorization than '\
                 'the key provides.' }, 401)
      end

      def fields(default)
        @fields ||= Filterator::V2.fields_from_filter(params[:filter] || default)
      end

      def role(*names)
        return if current_user.has_any_role?(*names)
        error!({ name: 'unauthorized', detail: 'The authenticated user does not have the required permissions for this action.' }, 403)
      end

      def trusted_key
        return if @key.is_trusted
        error!({ name: 'untrusted_key', detail: 'The presented key is not trusted.' }, 403)
      end

      def per_page
        [(params[:per_page] || 10).to_i, 100].min
      end

      def more?(col, **opts)
        ctr = if opts[:countable].nil? || opts[:countable] == true
                col.count
              else
                col.to_a.size
              end
        ctr > per_page * (params[:page]&.to_i || 1)
      end

      def paginated(col)
        col.paginate(page: params[:page], per_page: per_page)
      end

      def std_result(col, **opts)
        items = if !opts[:countable].nil? && !opts[:countable]
                  col
                else
                  paginated(col.select(fields(opts[:filter])))
                end
        more = if !opts[:countable].nil? && !opts[:countable]
                 opts[:more]
               else
                 more?(col, **opts)
               end
        { items: items, has_more: more }
      end

      def single_result(item, **_opts)
        { items: [item], has_more: false }
      end

      def render(template_name, **locals)
        file = Rails.root.join 'app/views', template_name
        template = File.read file
        ERB.new(template).result(OpenStruct.new(locals).instance_eval { binding })
      end
    end

    before do
      key = APIKey.find_by(key: params[:key])
      key.nil? ? SmokeDetector.find_by(access_token: params[:key]) : nil
      # smokey_token = key.nil? ? SmokeDetector.find_by(access_token: params[:key]) : nil
      # request_time ||= Time.now.to_f
      # uuid = request.env['action_dispatch.request_id']
      # redis_log_key = "request/#{request_time}/#{uuid}"
      # request.set_header 'redis_logs.log_key', redis_log_key
      # request.set_header 'redis_logs.timestamp', request_time
      # request.set_header 'redis_logs.request_id', uuid
      # request.set_header 'redis_logs.start_time', Time.now.to_f.to_s
      # RedisLogJob.perform_later(
      #   {
      #     path: request.path,
      #     api_key_id: key&.id,
      #     smoke_detector_id: smokey_token&.id,
      #     sha: CurrentCommit
      #   },
      #   subspaces: {
      #     request_headers: headers.except('Cookie'),
      #     params: params.except(:key)
      #   },
      #   time: request_time,
      #   uuid: uuid
      # )
      authenticate_app!
    end

    content_type :atom, 'application/atom+xml'
    content_type :rss, 'application/rss+xml'

    mount API::DebugAPI => 'v2.0/debug'
    mount API::AnnouncementsAPI => 'v2.0/announcements'
    mount API::AppsAPI => 'v2.0/apps'
    mount API::CommitStatusesAPI => 'v2.0/commits'
    mount API::DeletionLogsAPI => 'v2.0/deletions'
    mount API::DomainTagsAPI => 'v2.0/tags'
    mount API::FeedbacksAPI => 'v2.0/feedbacks'
    mount API::PostsAPI => 'v2.0/posts'
    mount API::ReasonsAPI => 'v2.0/reasons'
    mount API::SmokeDetectorsAPI => 'v2.0/smokeys'
    mount API::SpamDomainsAPI => 'v2.0/domains'
    mount API::UsersAPI => 'v2.0/users'
    mount API::ModeratorSitesAPI => 'v2.0/mods'
    mount API::SitesAPI => 'v2.0/sites'
    mount API::CommentsAPI => 'v2.0/comments'
  end
end


text:
# frozen_string_literal: true

module API
  class DebugAPI < API::Base
    get '/' do
      { params: params, ts: DateTime.now, filters: FILTERS }
    end

    get 'filter' do
      { fields: Filterator::V2.fields_from_filter(params[:filter]) }
    end
  end
end


text:
# frozen_string_literal: true

module API
  class ModeratorSitesAPI < API::Base
    get '/' do
      std_result ModeratorSite.all, filter: FILTERS[:mods]
    end

    get 'user/:id' do
      std_result ModeratorSite.where(user_id: params[:id]), filter: FILTERS[:mods]
    end
  end
end


text:
# frozen_string_literal: true

module API
  class DeletionLogsAPI < API::Base
    get '/' do
      std_result DeletionLog.all.order(id: :desc), filter: FILTERS[:deletions]
    end

    get '/post/:id' do
      std_result DeletionLog.where(post_id: params[:id]).order(id: :desc), filter: FILTERS[:deletions]
    end

    before do
      authenticate_user!
      trusted_key
    end

    params do
      requires :key, type: String
      requires :is_deleted, type: Boolean
      optional :uncertainty, type: Integer
      optional :timestamp, type: DateTime
    end

    post '/post/:id' do
      create_opts = { is_deleted: params[:is_deleted], post_id: params[:id], api_key: @key }
      create_opts[:uncertainty] = params[:uncertainty] if params[:uncertainty].present?
      create_opts[:created_at] = params[:timestamp] if params[:timestamp].present?
      log = DeletionLog.create(**create_opts)
      single_result log
    end
  end
end


text:
# frozen_string_literal: true

require 'commonmarker'

def render_markdown(text)
  CommonMarker.render_doc(text,
                          %i[LIBERAL_HTML_TAG STRIKETHROUGH_DOUBLE_TILDE],
                          %i[strikethrough autolink]).to_html(:UNSAFE)
end

output = %w[

Path	Description

]
Dir.glob('../docs/**/*.md').sort_by { |x| File.basename x }.each do |f|
  text = File.read f
  title = Regexp.new(/^# ?([\w ]+)$/).match(text)[1]

  output << "
#{title}
"

  routes = text.split("---\n").reject { |x| x.nil? || x == '' }
  routes.shift

  routes.each do |r|
    splat = r.split("\n")
    meta = splat.shift.split '|'
    url = meta[0]
    desc = meta[1]
    details = render_markdown(splat.join("\n"))
    output << "
#{url}	#{desc}#{details}
"
  end
end
output << '
'
output << '
'

puts output


text:
// Place all the styles related to the DomainGroups controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/


text:
// Place all the styles related to the feedbacks controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

.feedback-button {
  margin-left: 5px;
}

@media (pointer: coarse) {
  .feedback-button {
	  margin-left: 25px;
	  font-size: 20px;
  }
}

/* The following duplicates the above pointer: coarse @media query, because our SCCS
 * conversion doesn't understand "or" in @media queries.
 */
@media (max-width: 767px) {
  .feedback-button {
	  margin-left: 25px;
	  font-size: 20px;
  }
}


text:
// Place all the styles related to the MicroAuth controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/


text:
// Place all the styles related to the APIKeys controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/


text:
.qr-2fa {
  margin: 20px;
  margin-bottom: 35px;
}


text:
// Place all the styles related to the PostComments controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

.add-comment {
  display: none;
  margin-bottom: 1em;
}

.comment-control {
  margin-left: 1em;

  a, a:visited, a:active, a:hover {
    color: inherit;
    text-decoration: none;
  }
}

#post_comment_text {
  margin-bottom: 0.5em;
}

.post-comment .panel-body p:last-of-type {
  margin-bottom: 0;
}

.comment-hint {
  margin: 0 1em;
}


text:
// Place all the styles related to the CustomRegistrations controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/


text:
table.table.no-top-border th {
  border-top:none;
}


text:
// Place all the styles related to the CodeStatus controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

.code-status {

  details summary:focus {
    outline: 0;
  }

  .commit summary {
    font-size: 14px;
  }

  details:not([open]) #show-all-gem-versions {
    display: none;
  }
  #show-all-gem-versions {
    font-size: 0.5em;
    &::after {
      content: 'more';
    }
    &.shown::after {
      content: 'less';
    }
  }
  .diff-toggle {
    &::before {
      content: 'Show';
    }
    &.shown::before {
      content: 'Hide';
    }
    &.no-diff {
      color: gray;
      pointer-events: none;
      cursor: not-allowed;
      &::before {
        content: 'No';
      }
    }
  }
}

.user-mention {
  // stolen from GitHub
  font-weight: 600;
  color: #24292e !important;
  white-space: nowrap;
}


text:
// Place all the styles related to the ReviewQueues controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

.top-filters {
  margin: 1.5em 0;
}

/* Keep the "Skip" button on review pages in the same location with or without an NAA button. */
body.review-queues .review-item .panel-default .panel-body {
  a.btn[href*="response=fp"] + a.btn[href*="response=skip"] {
    margin-left: 92px;
  }

  /*Colors and icons for review buttons*/
  .btn.btn-primary.review-submit-link[href*="response=tp"] {
    background-color: #006800;
    border-color: #005800;
  }
  .btn.btn-primary.review-submit-link[href*="response=fp"] {
    background-color: #b00000;
    border-color: #a00000;
  }
  .btn.btn-primary.review-submit-link[href*="response=naa"] {
    background-color: #724E40;
    border-color: #623E30;
  }
  .btn.btn-primary.review-submit-link[href*="response=tp"]::after {
    content: "(\2713\2009)";
    margin-left: 5px;
  }
  .btn.btn-primary.review-submit-link[href*="response=fp"]::after {
    content: "(\2717\200A)";
    margin-left: 5px;
  }
  .btn.btn-primary.review-submit-link[href*="response=naa"]::after {
    content: "(\1f4a9\200A)";
    margin-left: 5px;
  }
}


text:
td label.permissions-checkbox-label {
  display: block;
}


text:
// Place all the styles related to the reasons controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

.post-body {
  margin-left: 20px;
  margin-top: 10px;
}
.report-reasons {
  background-color: lightyellow;
  padding: 5px;
}
.inactive-reason a {
  color: grey;
}


text:
// Place all the styles related to the CustomSessions controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/


text:
// Place all the styles related to the AbuseComments controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/


text:
// Place all the styles related to the DomainLinks controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

.domain-link {
  padding: 0.5em 0;
  border-top: 1px solid #ddd;
  border-bottom: 1px solid #ddd;

  & + .domain-link {
    border-top: 0;
  }

  blockquote {
    border-left: 4px solid #ddd;
    padding-left: 1em;
    font-size: 1em;
  }
}


text:
// Place all the styles related to the Statistics controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/


text:
// Place all the styles related to the SpamDomains controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

.add-domain-tag, .add-post-tag {
  margin-left: 15px;
}

.domain-tag-list {
  display: flex;
  align-items: center;
  width: 100%;

  p {
    margin-top: 7px;
  }
}

.domain-tag {
  color: white !important;
}

.tag-label form {
  display: inline;
}

.tag-label {
  margin: 0 5px;
}

.spam-domain {
  display: inline-block;
  padding: 0.5em 0;
}

/* Spam domains pages: more responsive */
body.spam-domains {
  h1 {
    word-break: break-all;
  }

  .domain-tag-list {
    display: block;

    > * {
      display: inline-block;
    }
  }

  table.table-striped {
    td:first-of-type {
      overflow-wrap: anywhere;
      word-break: break-all;
    }
    td > .tag-label {
      display: inline-block;
    }
  }

}


text:
// Place all the styles related to the AbuseReportStatuses controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

.report-status {
  font-size: 120% !important;
  padding: 0.5em !important;
  margin: 1em 1em;
  vertical-align: middle !important;

  &.nl {
    margin-left: 0;
  }

  a, a:visited, a:hover, a:active {
    color: inherit;
  }
}

h1 .report-status, h3 .report-status {
  font-size: 60% !important;
}

.status-row {
  margin: 2em 0;
}


text:
// Place all the styles related to the SpamWaves controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

/*Spam waves page: more horizontal, and responsive*/
body.spam-waves .spam-wave {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
}
body.spam-waves .spam-wave > h3 {
  width: 100%;
}
body.spam-waves .spam-wave > p:not(.text-muted) {
  white-space: nowrap;
  flex-basis: calc(min(38vw, 170px)); /* SCSS is unable to compile this without calc() */
}
body.spam-waves .spam-wave > .text-muted {
  width: calc(min(55vw, 400px));
  min-width: 300px;
}
body.spam-waves .spam-wave .btn {
  width: min-content;
  height: min-content;
  transform: translateY(-6px);
}
body.spam-waves .spam-wave .btn:not(:last-of-type) {
  margin-right: calc(min(1vw, 10px));
}
body.spam-waves h2 .button-container {
    margin-left: 2vw;
}

/*Spam Wave new and edit pages */
body.spam-waves .initial-content > .row > .col-md-8.col-md-offset-2 {
  margin-left: 0px;
  width: 100%;
}
body.spam-waves .initial-content > .row > .col-md-8.col-md-offset-2 .panel-body {
  max-width: unset;
}


text:
// Place all the styles related to the FlagLog controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/


text:
// Place all the styles related to the FlagSettings controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

.unsaved-changes {
  position: fixed;
  bottom: 0;
  z-index: 100;

  p {
    padding: 0.75em 1.25em;
  }
}


text:
// Place all the styles related to the RedisLog controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

.square-borders {
  border-radius: 0px !important;
  margin-bottom: -1px;
}

.list-item-label {
  background-color: #f5f5f5 !important;
  display: table !important;
  width: 100%;
  table-layout: fixed;
}

.list-item-label > a {
  overflow: hidden;
  white-space: nowrap;
  display: table-cell;
  text-overflow: ellipsis;
}

.list-item-label:hover > a, .list-item-label > a:hover {
  text-decoration: none;
}

.list-item-label > span {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.list-item-label > * {
  display: table-cell;
}

.request-details-table {
  tr > td:first-of-type {
    font-weight: bold;
  }

  td {
    padding: 0.5em;
  }
}


text:
// Place all the styles related to the SiteSettings controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

.editable-value {
  cursor: pointer;
}


text:
// Place all the styles related to the authentication controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/


text:
// Place all the styles related to the UserSiteSettings controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/


text:
// Place all the styles related to the AbuseReports controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

.report-title {
  margin-bottom: 0.6em;
}

.abuse-report {
  margin: 1em;

  h3 {
    margin-top: 5px;
  }
}


text:
// Place all the styles related to the Data controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

.templates {
  display: none;
}

.small-fc {
  max-width: 15em;
  display: inline-block !important;
}

.data-list-item {
  padding: 5px;
}

.data-run-error-popover.popover {
  max-width: 500px;
  &.bottom .arrow::after {
    border-bottom-color: #f5f5f5;
  }

  .popover-content {
    margin: -1px;
    border: 0;
    border-top: 1px solid #ccc;
    border-radius: 0 0 5px 5px;
  }
}


text:
// Place all the styles related to the posts preview here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

/* The CSS/SCSS code below is largely copied from FIRE, with a significant portion of that
 * being copied from Stack Exchange/Stacks.  FIRE
 * (https://github.com/Charcoal-SE/userscripts/blob/master/fire/fire.user.js) is under a
 * dual Apache License 2.0 and MIT license.  The intent is for the code below and the code
 * in FIRE to remain relatively in sync, but there are differences. Thus, this should
 * primarily remain as mostly CSS, so comparisons with FIRE are easier.
 */

.post-body-panel-preview > .panel > .panel-body {
  img {
    max-width: 100%;
  }
  img[src^='https://via.placeholder.com'] {
    cursor: pointer;
    box-shadow: 0 0 10px -2px #646464;
  }
  a {
    font-weight: bold;
    text-decoration: underline;
    padding: 1px 5px;
    border-radius: 3px;
    box-shadow: 0 0 10px -2px #646464;
    display: inline-block;
    word-wrap: anywhere;
  }
  a.onebox-link[data-onebox-id] {
    position: relative;
  }
  a.onebox-link[data-onebox-id][href*="github.com/"]:after {
    content: "GitHub onebox link";
    position: absolute;
    right: 0;
    top: -1.25em;
    font-size: 80%;
  }
  a img[src^='https://via.placeholder.com'] {
    box-shadow: unset;
  }
  li {
    margin-left: 10px;
  }
  h1 {
    font-size: 23px;
  }
  h2 {
    font-size: 21px;
  }
  h3 {
    font-size: 19px;
  }
  h4 {
    font-size: 17px;
  }
  h5 {
    font-size: 15px;
  }
  h6 {
    font-size: 13px;
  }
  h1,
  h2,
  h3,
  h4,
  h5,
  h6 {
    line-height: 1.3;
    margin: 0 0 .5em 0;
    font-weight: bold;
  }
  img + h1,
  img + h2,
  img + h3,
  img + h4,
  img + h5,
  img + h6,
  dl + h1,
  dl + h2,
  dl + h3,
  dl + h4,
  dl + h5,
  dl + h6,
  p + h1,
  p + h2,
  p + h3,
  p + h4,
  p + h5,
  p + h6,
  pre + h1,
  pre + h2,
  pre + h3,
  pre + h4,
  pre + h5,
  pre + h6,
  blockquote + h1,
  blockquote + h2,
  blockquote + h3,
  blockquote + h4,
  blockquote + h5,
  blockquote + h6,
  table + h1,
  table + h2,
  table + h3,
  table + h4,
  table + h5,
  table + h6,
  .s-table-container + h1,
  .s-table-container + h2,
  .s-table-container + h3,
  .s-table-container + h4,
  .s-table-container + h5,
  .s-table-container + h6,
  .s-link-preview + h1,
  .s-link-preview + h2,
  .s-link-preview + h3,
  .s-link-preview + h4,
  .s-link-preview + h5,
  .s-link-preview + h6,
  dd + h1,
  dd + h2,
  dd + h3,
  dd + h4,
  dd + h5,
  dd + h6,
  ul + h1,
  ul + h2,
  ul + h3,
  ul + h4,
  ul + h5,
  ul + h6,
  ol + h1,
  ol + h2,
  ol + h3,
  ol + h4,
  ol + h5,
  ol + h6 {
    margin-top: 1em;
  }
  a .href {
    display: none;
    word-break: break-all;
  }
  a:hover .href {
    display: inline;
  }
  a:hover .text {
    display: none;
  }
  pre {
    margin-bottom: 1em;
    width: auto;
    max-height: 100px;
    overflow: auto;
    font-family: Fira Code, Consolas, Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, sans-serif;
    font-size: 13px;
    background-color: #eff0f1;
    box-shadow: 0 0 10px -1px #646464;
    border-radius: 3px;
    padding: 6px 10px;
    -webkit-transition: max-height 0.4s;
    -moz-transition: max-height 0.4s;
    transition: max-height 0.4s;
    overflow-wrap: normal;
  }
  pre code {
    padding: 0;
  }
  .snippet-code {
    border: 1px solid #e4e6e8;
    padding: 10px;
  }
  code {
    font-family: Fira Code,Consolas,Menlo,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New,monospace,sans-serif;
    font-size: 13px;
    background-color: #eff0f1;
    padding: 1px 5px;
  }
  a.post-tag {
    display: inline-block;
    padding: .4em .5em;
    margin: 2px 2px 2px 0;
        font-size: 11px;
    line-height: 1;
    white-space: nowrap;
    text-decoration: none;
    text-align: center;
    border-width: 1px;
    border-style: solid;
    border-radius: 3px;
    box-shadow: unset;
    font-weight: unset;
  }
  a.post-tag {
    color: rgb(44, 88, 119);
    background-color: rgb(225, 236, 244);
    border-color: rgb(225, 236, 244);
  }
  a.post-tag:hover {
    color: rgb(57, 115, 157);
    background-color: rgb(208, 227, 241);
    border-color: rgb(208, 227, 241);
  }
  a.post-tag.required-tag {
    color: rgb(59, 64, 69);
    background-color: rgb(227, 230, 232);
    border-color: rgb(186, 191, 196);
  }
  a.post-tag.required-tag:hover {
    color: rgb(35, 38, 41);
    background-color: rgb(214, 217, 220);
    border-color: rgb(159, 166, 173);
  }
  a.post-tag.moderator-tag {
    color: rgb(194, 46, 50);
    background-color: rgb(253, 242, 242);
    border-color: rgb(249, 210, 211);

  }
  a.post-tag.moderator-tag:hover {
    color: rgb(171, 38, 42);
    background-color: rgb(249, 210, 211);
    border-color: rgb(244, 180, 182);
  }
  blockquote {
    quotes: none;
    position: relative;
    margin: 0 1em 1.1em 1em;
    padding: .8em .8em .8em 1em;
    color: #535a60;
    background-color: #fbf2d4;
    color: #0d0e0f;
    border-left: 2px solid #ffeb8e;
  }
  blockquote::before {
    content: "";
    display: block;
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    width: 4px;
    border-radius: 8px;
    background: #c8ccd0;
  }
  .spoiler {
    background: #eff0f1;
    border-radius: 5px;
    color: #242729;
    cursor: pointer;
    min-height: 48px;
  }
  .spoiler::after {
    content: attr(data-spoiler) " ";
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' aria-hidden='true' fill='rgb(132, 141, 149)' width='18' height='18' viewBox='0 0 18 18'%3E%3Cpath d='M9 17A8 8 0 119 1a8 8 0 010 16zM8 4v6h2V4H8zm0 8v2h2v-2H8z'%3E%3C/path%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: center right;
    font-size: 13px;
    color: #6a737c;
    padding-right: 22px;
    position: absolute;
    top: 1em;
    right: 1em;
    transition: opacity .1s ease-in-out;
    pointer-events: none;
  }
  .spoiler > * {
    visibility: hidden;
    opacity: 0;
    transition: opacity .1s ease-in-out;
  }
  .spoiler.is-visible {
    cursor: auto;
  }
  .spoiler.is-visible > * {
    visibility: visible;
    opacity: 1;
  }
  .spoiler.is-visible::after {
    opacity: 0;
  }
  .s-table-container {
        margin-bottom: var(--s-prose-spacing);
    overflow-x: auto;
    scrollbar-color: var(--scrollbar) transparent;
  }
  .s-table-container:last-child,
  .s-table-container:only-child {
    margin-bottom: 0;
  }
  .s-table-container::-webkit-scrollbar {
    width: 10px;
    height: 10px;
    background-color: transparent;
  }
  .s-table-container::-webkit-scrollbar-track {
    border-radius: 10px;
    background-color: transparent;
  }
  .s-table-container::-webkit-scrollbar-thumb {
    border-radius: 10px;
    background-color: var(--scrollbar);
  }
  .s-table-container::-webkit-scrollbar-corner {
    background-color: transparent;
    border-color: transparent;
  }
  .s-table {
    display: table;
    width: 100%;
    max-width: 100%;
    border-collapse: collapse;
    border-spacing: 0;
    font-size: 13px;
  }
  .s-table th,
  .s-table td {
    padding: 8px;
    border-top: 1px solid var(--black-100);
    border-left: 1px solid var(--black-100);
    border-right: 1px solid var(--black-100);
    vertical-align: middle;
    color: var(--fc-medium);
    text-align: left;
  }
  .s-table th {
    font-weight: bold;
    color: var(--fc-dark);
  }
  .s-table thead th {
    vertical-align: bottom;
    white-space: nowrap;
    background-color: var(--black-025);
    line-height: 1.15384615;
  }
  .s-table tbody th {
    font-weight: normal;
  }
  .s-table tr:last-of-type td,
  .s-table tr:last-of-type th {
    border-bottom: 1px solid var(--black-100);
  }
  .s-table tbody + tbody {
    border-top: 2px solid var(--black-100);
  }
  kbd {
    border: 1px solid rgb(159, 166, 173);
    border-top-color: rgb(159, 166, 173);
    box-shadow: rgba(12, 13, 14, 0.15) 0px 1px 1px 0px, rgb(255, 255, 255) 0px 1px 0px 0px inset;
    background-color: rgb(227, 230, 232);
    border-radius: 3px;
    color: rgb(35, 38, 41);
    display: inline-block;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI Adjusted", "Segoe UI", "Liberation Sans", sans-serif;
    font-size: 12px;
    line-height: 1.5;
    margin: 0 .1em;
    overflow-wrap: break-word;
    padding: .1em .6em;
    text-shadow: rgb(255, 255, 255) 0px 1px 0px;
  }
} /* .post-body-panel-preview > .panel > .panel-body */

/* Used in table CSS; Largely copied from SE's Stacks CSS*/
body,
.theme-light__forced {
  --black-025: #fafafb;
  --black-100: #d6d9dc;
  --red-075: #f9d3d780;
  --fc-dark: hsl(210, 8%, 5%);
  --fc-medium: hsl(210, 8%, 25%);
  --fc-light: hsl(210, 8%, 45%);
  --scrollbar: hsla(0, 0%, 0%, 0.2);
}

/* END code copied from FIRE */

/* The following applies CSS for MS and overrides some that's in the FIRE CSS.
   It's done this way to make comparison with the CSS in FIRE easier.
 */

.posts-table {
  table-layout: fixed;
}
.post-body-pre-block {
  display: block;
}

.post-body-panel-preview > .panel > .panel-body {
  display: block;
  overflow: auto;

  pre {
    max-height: unset;
    box-shadow: unset;
    border-radius: unset;
    -webkit-transition: unset;
    -moz-transition: unset;
    transition: unset;
    background-color: #f7f7f7;
  }
  pre code {
    background-color: transparent;
    display: block;
  }
  code {
    color: #111;
  }
  a {
    font-weight: unset;
    box-shadow: unset;
    display: unset;
  }
  a.post-tag {
    font-size: 12px;
  }
  blockquote {
    background-color: #fbf2d4a8;
    font-size: unset;
    border-left: unset;
  }
  .s-table-container {
    margin-bottom: 1.1em;
  }
  /* Use the same font sizes for headings as Stack Exchange does. */
  h1 {
    font-size: 27px;
  }
  h2 {
    font-size: 21px;
  }
  h3 {
    font-size: 19px;
  }
  h4 {
    font-size: 17px;
  }
  h5 {
    font-size: 15px;
  }
  h6 {
    font-size: 13px;
  }
}


text:
// Place all the styles related to the SmokeDetectors controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/


text:
// Place all the styles related to the graphs controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/


text:
/*
 * This is a manifest file that'll be compiled into application.css, which will include all the files
 * listed below.
 *
 * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
 * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
 *
 * You're free to add application-wide styles to this file and they'll appear at the bottom of the
 * compiled file so the styles you add here take precedence over styles defined in any styles
 * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
 * file per style scope.
 *
 *= require_tree .
 *= require_self
 */

body {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

.initial-content {
  flex: 1 0 auto;
}

.footer {
  width: 100%;
  background-color: #fafafa;

  margin-top: 20px;

  p {
    padding: 20px;
  }
}

.col-centered {
  float: none !important;
  margin: 0 auto;
}

.login-box {
  margin-bottom: 10px;

  .btn-primary {
    width: 100%
  }
}
#devise-links a {
  color: grey !important
}

@media (min-width: 768px) {
  .navbar-form {
    margin: 0;
    padding-left: 0;
    position: absolute;
    right: 15px;
    z-index: 1;
    pointer-events: none;
    .form-group {
      height: 38px;
      display: flex;
      align-items: center;
      #search {
        width: 250px;
        box-sizing: border-box;
        pointer-events: initial;
        & {
          transition: all 0.4s;
        }
        &:not(:focus) {
          pointer-events: none;
          width: 25px !important;
          opacity: 0;
        }
      }
    }
  }
}

.sortable-table th:not(.no-sort) {
  cursor: pointer;
  padding-left: 1em;
  position: relative;

  &::before, &::after {
    content: "";
    position: absolute;
    left: 0;
    display: inline-block;
    width: 0;
    height: 0;
    border-width: 4px;
    border-style: none solid;
    border-color: #999;
    border-right-color: transparent;
    border-left-color: transparent;
  }
  &::before {
    border-bottom-style: solid;
    bottom: 1.4em;
  }
  &::after {
    bottom: 0.9em;
    border-top-style: solid;
  }
}

.login-box form {
  border: 1px solid lightgrey;
  padding: 20px;
  border-radius: 4px;
}

.se-oauth-button {
  width: 100%;
  margin-bottom: 20px !important;
}

.se-icon {
  height: 22px;
}

.se-auth-or-label {
  text-align: center;
  font-variant: small-caps;
  font-weight: bold;
  font-size: 1.35em;

  &:before, &:after {
    content: '-';
    display: inline-block;
    margin: 0.5em;
    font-weight: normal;
  }
}

.dropdown-indent a {
  padding-left: 30px !important;
}

.navbar-header {
  display: flex;
  align-items: center;
  @media (max-width: 768px) {
    .navbar-brand {
      flex: 1;
    }
    .navbar-toggle {
      order: 1; // flexbox version of float: right
      &::before {
        content: '';
        position: absolute;
        top: -0.4em;
        left: -0.4em;
        width: 0.75em;
        height: 0.75em;
        border-radius: 50%;
      }
    }
  }
}

body .profiler-results {
  position: absolute;
  top: 52px !important;
}

#metasmoke-deployed-banner {
  position: -webkit-sticky;
  position: sticky;
  top: 15px;
  z-index: 1048576;
}

.module {
  border: 1px solid lightgray;
  box-shadow: 2px 2px 7px lightgray;
  border-radius: 4px;
  padding-bottom: 3px;
}

.module-green {
  border-top: 4px solid green;
  padding-bottom: 0;
}

.module-green .status-text {
  color: green;

  a, a:visited {
    color: green;
    text-decoration: none;
    font-weight: bold;
  }

  a:hover, a:active {
    color: green;
    text-decoration: underline;
    font-weight: bold;
  }
}

.module-amber {
  border-top: 4px solid orange;
  padding-bottom: 0;
}

.module-amber .status-text {
  color: orange;

  a, a:visited {
    color: orange;
    text-decoration: none;
    font-weight: bold;
  }

  a:hover, a:active {
    color: orange;
    text-decoration: underline;
    font-weight: bold;
  }
}

.module-red {
  border-top: 4px solid red;
  padding-bottom: 0;
}

.module-red .status-text {
  color: red;

  a, a:visited {
    color: red;
    text-decoration: none;
    font-weight: bold;
  }

  a:hover, a:active {
    color: red;
    text-decoration: underline;
    font-weight: bold;
  }
}

.module-header {
  padding: 3px 10px;
  width: 100%;
  border-bottom: 1px solid lightgray;
}

.module-title {
  font-variant: small-caps;

  a, a:visited {
    color: #333;
    text-decoration: none;
  }

  a:hover, a:active {
    color: #333;
    text-decoration: underline;
  }
}

.module-body {
  padding: 10px;
}

.form-submit {
  cursor: pointer;
  font-weight: bold;
}

.panel-body h3 {
  margin-top: 0;
}

/*Temporary fix for dropdown menus:*/
.dropdown-menu.inner {
  position: unset;
}

.dropdown-menu.open > .inner.open {
  overflow-x: hidden !important;
}

.filter-option {
  display: inline-block;
  width: 100%;
  overflow-x: hidden;
  margin-right: 10px;
  vertical-align: bottom;
}

.dropdown.bootstrap-select.bs3 {
  display: inline-block;
}

.dropdown-menu.inner {
  small.text-muted {
    padding-left: 5px;
  }

  a {
    min-height: 26px;
  }

  .no-results {
    padding: 3px;
    background: #f5f5f5;
    margin: 0 5px;
    white-space: nowrap;
  }
}

.dropdown-menu > .active > a .text-muted, .dropdown-menu > .active > a:focus .text-muted, .dropdown-menu > .active > a:hover .text-muted {
  color: #ddd;
}

.domain-tag-list {
  position: relative;

  form.review-add-domain-tag .dropdown,
  form.add-domain-tag .dropdown {
    position: unset;
  }

  /*Keep the dropdown a semi-reasonable size*/
  form.review-add-domain-tag .dropdown-menu.open,
  form.add-domain-tag .dropdown-menu.open {
    max-width: 90%;
    left: 5%;
    max-height: 90vh !important; /* This property is assigned by JavaScript on the element. */
  }
}

/* Tag formatting in domain tag dropdown */
.domain-tag-list {
  button:not(.bs-placeholder) .filter-option-inner-inner {
    padding-top: 0px;
    padding-bottom: 3px;
  }

  .dropdown-menu.inner li a:not([id$="-0"]) .text {
    padding-top: 1px;
    padding-bottom: 3px;
  }

  button:not(.bs-placeholder) .filter-option-inner-inner,
  .dropdown-menu.inner li a:not([id$="-0"]) .text {
    background-color: #5bc0de;
    color: white;
    padding-left: 5px;
    vertical-align: text-bottom;
  }

  button:not(.bs-placeholder) .filter-option-inner-inner.special-tag,
  .dropdown-menu.inner li a.special-tag:not([id$="-0"]) .text {
    background-color: #f0ad4e;
  }

  .filter-option {
    height:20px;
  }

  .filter-option-inner-inner {
    display: inline-block;
  }

  button:not(.bs-placeholder) .filter-option-inner-inner:before,
  .dropdown-menu.inner li a:not([id$="-0"]) .text::before {
    content: "#";
    margin-right: 1px;
  }

  .dropdown-menu.inner {
    a .text small {
      font-weight: normal;
      margin-left: 5px;
      background-color: white;
      border: 5px solid white;
    }

    a:hover .text small {
      background-color: #f5f5f5;
      border-color: #f5f5f5;
    }

    li.active a .text small {
      background-color: #337ab7;
      border-color: #337ab7;
    }
  }

  .dropdown-menu > .active > a, .dropdown-menu > .active > a:focus, .dropdown-menu > .active > a:hover {
    padding-bottom: 4px;
  }

  .filter-option-inner-inner small {
    background-color: white;
    border: 5px solid white;
    margin-left: 5px;
  }

  .dropdown.bootstrap-select.open button:hover .filter-option-inner-inner small,
  .dropdown.bootstrap-select.open button:focus .filter-option-inner-inner small,
  button:active:hover .filter-option-inner-inner small {
    background-color: #d4d4d4;
    border-color: #d4d4d4;
  }

  .dropdown.bootstrap-select.open .filter-option-inner-inner small,
  button:active .filter-option-inner-inner small,
  button:focus .filter-option-inner-inner small,
  button:hover .filter-option-inner-inner small {
    background-color: #e6e6e6;
    border-color: #e6e6e6;
  }
}

/* Multi-select dropdown (e.g. Sites) with checkmark indicating selection */
.dropdown-menu.inner {
  li:not(.selected) .glyphicon.glyphicon-ok.check-mark {
    display: none;
  }
  li .glyphicon.glyphicon-ok.check-mark {
    margin-right: 4px;
    margin-left: -18px;
  }
}

.icon-nav-bar {
  padding: 0;
  position: absolute;
  border: solid lightgray 1px;
  border-top: none;
  border-left: none;
  border-bottom-right-radius: 4px;
  margin-top: 1px;
}
.icon-nav-bar li a {
  padding: 0.7em;
}

.wait-for-AJAX.wait-for-AJAX {
  cursor: progress;
}

/* Search and flagging back in topbar, but responsive */
.navbar-static-top {
  .navbar-nav > li > a.glyphicon {
      display: none;
  }
  @media (min-width: 768px) {
    @media (max-width: 1000px) {
      .navbar-nav {
        > li > a.glyphicon {
          display: inline-block;
          min-width: 40px;
          text-align: center;
        }
        > li:is(.nav-tab-flagging, .nav-tab-search) > a:not(.glyphicon) {
          display: none;
        }
        > li > a[href="/review"] {
          position: relative;
          > .reviews-count {
              position: absolute;
              bottom: 0;
              left: 2.3em;
              transform: scale(.9) translateX(-50%);
          }
        }
      }
      .home.inline {
        margin-left: 0;
        span {
          padding-left: .5vw;
        }
        + .navbar-brand {
          padding-left: 1vw;
          padding-right: 0.5vw;
        }
      }
      .container-fluid {
        padding-left: .5vw;
        padding-right: .5vw;
      }
      .navbar-right.navbar-right {
        margin-right: 0px;
      }
      .navbar-right > li > a[href="#"] {
        margin-right: calc(.5vw + 3px); /* +3px to compensate for logo img internal border */
      }
    }
    @media (max-width: 1100px) {
      .commit-sha.commit-sha {
        position: absolute;
        top: 0;
        right: 0;
        height: min-content !important;
        width: min-content;
        > a {
          padding: 0 2px 0 0 !important;
          line-height: 10px;
          > code {
            padding: 0;
          }
        }
      }
      .container-fluid .navbar-collapse .navbar-nav:not(.navbar-right) {
        justify-content: space-around;
      }
    }
    .navbar-right > li > a[href="#"] {
      white-space: break-spaces;
      word-break: break-word;
      overflow-wrap: break-word;
      padding-bottom: 0px;
      height: 50px;
      overflow: hidden;
      text-overflow: ellipsis;
      line-height: 16px;
      padding-top: 17px;
      display: -webkit-box;
      -webkit-line-clamp: 2;
      -webkit-box-orient: vertical;
      padding-right: calc(1em + MIN(0.9vw, 15px));
    }
    .nav > li > a {
      padding-left: MIN(.9vw, 15px);
      padding-right: MIN(.9vw, 15px);
    }
    .container-fluid {
      display: flex;
      .navbar-collapse {
        display: flex !important;
        justify-content: space-between;
        flex-grow: 1;
        > .navbar-nav {
          flex-grow: 10;
          display: flex;
          > li {
            white-space: nowrap;
            height: 50px;
          }
        }
        .navbar-right {
          justify-content: flex-end;
          display: flex;
          > li > a[href="#"] {
            .caret {
              display: none;
            }
            /*Simulate .caret*/
            &:after {
              content: " ";
              position: absolute;
              top: 24px;
              right: MIN(0.9vw, 15px);
              display: inline-block;
              width: 0;
              height: 0;
              margin-left: 2px;
              border-top: 4px solid;
              border-right: 4px solid transparent;
              border-left: 4px solid transparent;
            }
          }
        }
      }
    }
    .home.inline span {
      padding-left: .5vw;
    }
  }
  @media (min-width: 1001px) and (max-width: 1100px) {
    .navbar-right > li > a[href="#"] {
      margin-right: 19px; /* +4px to compensate for logo img internal border */
    }
  }
  @media (min-width: 1101px) {
    .container-fluid .navbar-collapse .navbar-nav:not(.navbar-right) {
      justify-content: flex-start;
    }
  }
}


text:
// Place all the styles related to the FlagConditions controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/


text:
// Place all the styles related to the status controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

$good: green;
$warning: orange;
$critical: red;

.status-good {
  color: $good !important;
  a {
    border-bottom: 3px solid $good;
    padding-bottom: 12px !important;
  }
  &.navbar-toggle::before {
    background: $good;
  }
}
.status-warning {
  color: $warning !important;
  a {
    border-bottom: 3px solid $warning;
    padding-bottom: 12px !important;
  }
  &.navbar-toggle::before {
    background: $warning;
  }
}
.status-critical {
  color: $critical !important;
  a {
    border-bottom: 3px solid $critical;
    padding-bottom: 12px !important;
  }
  &.navbar-toggle::before {
    background: $critical;
  }
}


text:
// Place all the styles related to the Posts controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

.show-post-body {
  cursor: pointer;
}

// Shamelessly stolen from http://stackoverflow.com/a/248013/1849664. Go upvote that answer.
pre.post-body-pre-block {
  white-space: pre-wrap;       /* CSS 3 */
  white-space: -moz-pre-wrap;  /* Mozilla, since 1999 */
  white-space: -pre-wrap;      /* Opera 4-6 */
  white-space: -o-pre-wrap;    /* Opera 7 */
  word-wrap: break-word;       /* Internet Explorer 5.5+ */
}

blockquote {
  font-size: inherit;
}

img {
  max-width: 100%;
}

.post-body {
  clear: both;
}

.post-row-right {
  float: right;
}

body.posts .bootstrap-select .btn.dropdown-toggle {
  padding-top: 4px;
  padding-bottom: 4px;
}

.flag-link {
  margin-left: 2em;
  margin-right: 2em;
}

.posts-table {
  table-layout: fixed;
}

.post-td:first-child:last-child {
	width: 100%;
}


text:
// Place all the styles related to the announcements controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

.datetime {
  max-width: 30em;
}