Full Code of usefathom/fathom for AI

master 2d895d8299d3 cached
181 files
342.0 KB
131.7k tokens
409 symbols
1 requests
Download .txt
Showing preview only (385K chars total). Download the full file or copy to clipboard to get everything.
Repository: usefathom/fathom
Branch: master
Commit: 2d895d8299d3
Files: 181
Total size: 342.0 KB

Directory structure:
gitextract_bylwszv8/

├── .env.example
├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       ├── goimports.yml
│       ├── release.yml
│       └── test.yml
├── .gitignore
├── .goreleaser.yml
├── CODE_OF_CONDUCT.md
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── assets/
│   └── src/
│       ├── 404.html
│       ├── css/
│       │   ├── chart.css
│       │   ├── fonts-overpass.css
│       │   ├── pikaday.css
│       │   ├── styles.css
│       │   └── util.css
│       ├── index.html
│       └── js/
│           ├── components/
│           │   ├── Chart.js
│           │   ├── CountWidget.js
│           │   ├── DatePicker.js
│           │   ├── Gearwheel.js
│           │   ├── LoginForm.js
│           │   ├── LogoutButton.js
│           │   ├── Notification.js
│           │   ├── Pikadayer.js
│           │   ├── Realtime.js
│           │   ├── Sidebar.js
│           │   ├── SiteSettings.js
│           │   ├── SiteSwitcher.js
│           │   └── Table.js
│           ├── lib/
│           │   ├── client.js
│           │   ├── numbers.js
│           │   └── util.js
│           ├── pages/
│           │   ├── dashboard.js
│           │   └── login.js
│           ├── script.js
│           └── tracker.js
├── docker-compose.yml
├── docs/
│   ├── Configuration.md
│   ├── FAQ.md
│   ├── Installation instructions.md
│   ├── README.md
│   ├── Updating to the latest version.md
│   └── misc/
│       ├── Heroku.md
│       ├── NGINX.md
│       └── Systemd.md
├── go.mod
├── go.sum
├── gulpfile.js
├── main.go
├── package.json
└── pkg/
    ├── aggregator/
    │   ├── aggregator.go
    │   ├── aggregator_test.go
    │   ├── bindata.go
    │   ├── blacklist.go
    │   ├── blacklist_test.go
    │   ├── data/
    │   │   └── blacklist.txt
    │   └── store.go
    ├── api/
    │   ├── api.go
    │   ├── auth.go
    │   ├── auth_test.go
    │   ├── collect.go
    │   ├── collect_test.go
    │   ├── health.go
    │   ├── http.go
    │   ├── http_test.go
    │   ├── page_stats.go
    │   ├── params.go
    │   ├── params_test.go
    │   ├── referrer_stats.go
    │   ├── routes.go
    │   ├── site_stats.go
    │   └── sites.go
    ├── cli/
    │   ├── cli.go
    │   ├── server.go
    │   ├── stats.go
    │   └── user.go
    ├── config/
    │   ├── config.go
    │   └── config_test.go
    ├── datastore/
    │   ├── datastore.go
    │   └── sqlstore/
    │       ├── config.go
    │       ├── config_test.go
    │       ├── hostnames.go
    │       ├── migrations/
    │       │   ├── mysql/
    │       │   │   ├── 10_alter_stats_table_constraints.sql
    │       │   │   ├── 11_add_pageview_finished_column.sql
    │       │   │   ├── 12_create_hostnames_table.sql
    │       │   │   ├── 13_create_unique_hostname_index.sql
    │       │   │   ├── 14_create_pathnames_table.sql
    │       │   │   ├── 15_create_unique_pathname_index.sql
    │       │   │   ├── 16_fill_hostnames_table.sql
    │       │   │   ├── 17_fill_pathnames_table.sql
    │       │   │   ├── 18_alter_page_stats_table.sql
    │       │   │   ├── 19_alter_referrer_stats_table.sql
    │       │   │   ├── 1_initial_tables.sql
    │       │   │   ├── 20_recreate_stats_indices.sql
    │       │   │   ├── 21_alter_page_stats_table.sql
    │       │   │   ├── 22_alter_site_stats_table.sql
    │       │   │   ├── 23_alter_referrer_stats_table.sql
    │       │   │   ├── 24_recreate_stat_table_indices.sql
    │       │   │   ├── 2_known_durations_column.sql
    │       │   │   ├── 3_referrer_group_column.sql
    │       │   │   ├── 4_pageview_id_column.sql
    │       │   │   ├── 5_create_sites_table.sql
    │       │   │   ├── 6_add_site_tracking_id_column_to_pageviews_table.sql
    │       │   │   ├── 7_add_site_id_to_site_stats_table.sql
    │       │   │   ├── 8_add_site_id_to_page_stats_table.sql
    │       │   │   └── 9_add_site_id_to_referrer_stats_table.sql
    │       │   ├── postgres/
    │       │   │   ├── 10_alter_numeric_column_precision.sql
    │       │   │   ├── 11_alter_stats_table_constraints.sql
    │       │   │   ├── 12_add_pageview_finished_column.sql
    │       │   │   ├── 13_create_hostnames_table.sql
    │       │   │   ├── 14_create_unique_hostname_index.sql
    │       │   │   ├── 15_create_pathnames_table.sql
    │       │   │   ├── 16_create_unique_pathname_index.sql
    │       │   │   ├── 17_fill_hostnames_table.sql
    │       │   │   ├── 18_fill_pathnames_table.sql
    │       │   │   ├── 19_alter_page_stats_table.sql
    │       │   │   ├── 1_initial_tables.sql
    │       │   │   ├── 20_alter_referrer_stats_table.sql
    │       │   │   ├── 21_recreate_stats_indices.sql
    │       │   │   ├── 22_alter_page_stats_table.sql
    │       │   │   ├── 23_alter_referrer_stats_table.sql
    │       │   │   ├── 24_alter_site_stats_table.sql
    │       │   │   ├── 25_recreate_stat_table_indices.sql
    │       │   │   ├── 26_alter_pageviews_table.sql
    │       │   │   ├── 2_known_durations_column.sql
    │       │   │   ├── 3_referrer_group_column.sql
    │       │   │   ├── 4_pageview_id_column.sql
    │       │   │   ├── 5_create_sites_table.sql
    │       │   │   ├── 6_add_site_tracking_id_column_to_pageviews_table.sql
    │       │   │   ├── 7_add_site_id_to_site_stats_table.sql
    │       │   │   ├── 8_add_site_id_to_page_stats_table.sql
    │       │   │   └── 9_add_site_id_to_referrer_stats_table.sql
    │       │   └── sqlite3/
    │       │       ├── 10_alter_stats_table_constraints.sql
    │       │       ├── 11_add_pageview_finished_column.sql
    │       │       ├── 12_create_hostnames_table.sql
    │       │       ├── 13_create_unique_hostname_index.sql
    │       │       ├── 14_create_pathnames_table.sql
    │       │       ├── 15_create_unique_pathname_index.sql
    │       │       ├── 15_vacuum.sql
    │       │       ├── 16_fill_hostnames_table.sql
    │       │       ├── 17_fill_pathnames_table.sql
    │       │       ├── 18_alter_page_stats_table.sql
    │       │       ├── 19_alter_referrer_stats_table.sql
    │       │       ├── 1_initial_tables.sql
    │       │       ├── 20_recreate_stats_indices.sql
    │       │       ├── 21_alter_page_stats_table.sql
    │       │       ├── 22_alter_site_stats_table.sql
    │       │       ├── 23_alter_referrer_stats_table.sql
    │       │       ├── 24_recreate_stat_table_indices.sql
    │       │       ├── 25_vacuum.sql
    │       │       ├── 26_sites_id_autoinc.sql
    │       │       ├── 2_known_durations_column.sql
    │       │       ├── 3_referrer_group_column.sql
    │       │       ├── 4_pageview_id_column.sql
    │       │       ├── 5_create_sites_table.sql
    │       │       ├── 6_add_site_tracking_id_column_to_pageviews_table.sql
    │       │       ├── 7_add_site_id_to_site_stats_table.sql
    │       │       ├── 8_add_site_id_to_page_stats_table.sql
    │       │       └── 9_add_site_id_to_referrer_stats_table.sql
    │       ├── page_stats.go
    │       ├── pageviews.go
    │       ├── pathnames.go
    │       ├── referrer_stats.go
    │       ├── seed/
    │       │   └── pageviews.sql
    │       ├── site_stats.go
    │       ├── sites.go
    │       ├── sqlstore.go
    │       └── users.go
    └── models/
        ├── page_stats.go
        ├── page_stats_test.go
        ├── pageview.go
        ├── referrer_stats.go
        ├── referrer_stats_test.go
        ├── site.go
        ├── site_stats.go
        ├── site_stats_test.go
        ├── user.go
        └── user_test.go

================================================
FILE CONTENTS
================================================

================================================
FILE: .env.example
================================================
FATHOM_GZIP=true
FATHOM_DEBUG=true
FATHOM_DATABASE_DRIVER="sqlite3"
FATHOM_DATABASE_NAME="./fathom.db"
FATHOM_DATABASE_USER=""
FATHOM_DATABASE_PASSWORD=""
FATHOM_DATABASE_HOST=""
FATHOM_SECRET="abcdefghijklmnopqrstuvwxyz1234567890"


================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
custom: ['https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=LJ5WZVA9ER9GJ']


================================================
FILE: .github/workflows/goimports.yml
================================================
name: Check imports
on: [ push, pull_request ]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/setup-go@v2
      with:
        go-version: '1.19'
    - uses: actions/checkout@master
    - name: Check imports
      shell: bash
      run: |
        export PATH=$(go env GOPATH)/bin:$PATH
        go get golang.org/x/tools/cmd/goimports
        diff -u <(echo -n) <(goimports -d .)


================================================
FILE: .github/workflows/release.yml
================================================
name: Release
on:
  push:
    tags:
      - 'v*'
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
        with:
          fetch-depth: 0
      - name: Set up Go
        uses: actions/setup-go@v3
        with:
            go-version: '1.19'
      - name: Run GoReleaser
        uses: goreleaser/goreleaser-action@v3
        with:
          # either 'goreleaser' (default) or 'goreleaser-pro'
          distribution: goreleaser
          version: latest
          args: release --rm-dist -p 1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .github/workflows/test.yml
================================================
---
name: Run tests
on: [ push, pull_request ]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/setup-go@v2
        with:
          go-version: '1.19'
      - uses: actions/checkout@master
      - name: Run tests
        run: |
          make test


================================================
FILE: .gitignore
================================================
node_modules
.env*
!.env.example
coverage.out
build
dist
*.db
fathom
!cmd/fathom

assets/build
assets/dist
bin/


================================================
FILE: .goreleaser.yml
================================================
# Documentation http://goreleaser.com
before:
  hooks:
    - make assets/dist
    - go install github.com/gobuffalo/packr/v2/packr2@latest
builds:
  - main: main.go
    goos:
      - linux
    goarch:
      - amd64
      - 386
      - arm64
    ldflags:
      - -extldflags "-static" -s -w -X "main.version={{.Version}}" -X "main.commit={{.Commit}}" -X "main.date={{.Date}}"
    hooks:
      pre: packr2
      post: packr2 clean
checksum:
  name_template: 'checksums.txt'
snapshot:
  name_template: "{{ .Tag }}-next"
changelog:
  sort: asc
  filters:
    exclude:
      - '^docs:'
      - '^test:'
release:
  draft: true
  mode: append


================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct

## Our Pledge

In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
education, socio-economic status, nationality, personal appearance, race,
religion, or sexual identity and orientation.

## Our Standards

Examples of behavior that contributes to creating a positive environment
include:

* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members

Examples of unacceptable behavior by participants include:

* The use of sexualized language or imagery and unwelcome sexual attention or
  advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
  address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
  professional setting

## Our Responsibilities

Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.

Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.

## Scope

This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at team@usefathom.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.

Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html

[homepage]: https://www.contributor-covenant.org


================================================
FILE: Dockerfile
================================================
FROM node:alpine AS assetbuilder
WORKDIR /app
COPY package*.json ./
COPY gulpfile.js ./
COPY assets/ ./assets/
RUN npm install && NODE_ENV=production ./node_modules/gulp/bin/gulp.js

FROM golang:latest AS binarybuilder
RUN go install github.com/gobuffalo/packr/v2/packr2@latest
WORKDIR /go/src/github.com/usefathom/fathom
COPY . /go/src/github.com/usefathom/fathom
COPY --from=assetbuilder /app/assets/build ./assets/build
ARG GOARCH=amd64
ARG GOOS=linux
RUN make ARCH=${GOARCH} OS=${GOOS} docker

FROM alpine:latest
EXPOSE 8080
HEALTHCHECK --retries=10 CMD ["wget", "-qO-", "http://localhost:8080/health"]
RUN apk add --update --no-cache bash ca-certificates
WORKDIR /app
COPY --from=binarybuilder /go/src/github.com/usefathom/fathom/fathom .
CMD ["./fathom", "server"]


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) Conva Ventures Inc

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: Makefile
================================================
EXECUTABLE := fathom
LDFLAGS += -extldflags "-static" -X "main.version=$(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')" -X "main.commit=$(shell git rev-parse HEAD)"  -X "main.date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
MAIN_PKG := ./main.go
PACKAGES ?= $(shell go list ./... | grep -v /vendor/)
ASSET_SOURCES ?= $(shell find assets/src/. -type f)
GO_SOURCES ?= $(shell find . -name "*.go" -type f)
GOPATH=$(shell go env GOPATH)
ARCH := amd64
OS := linux

.PHONY: all
all: build 

.PHONY: build
build: $(EXECUTABLE)

$(EXECUTABLE): $(GO_SOURCES) assets/build
	go build -o $@ $(MAIN_PKG)

.PHONY: docker
docker: $(GO_SOURCES) $(GOPATH)/bin/packr2
	GOOS=$(OS) GOARCH=$(ARCH) $(GOPATH)/bin/packr2 build -v -ldflags '-w $(LDFLAGS)' -o $(EXECUTABLE) $(MAIN_PKG)

$(GOPATH)/bin/packr2:
	GOBIN=$(GOPATH)/bin go install github.com/gobuffalo/packr/v2/packr2@latest

.PHONY: npm 
npm:
	if [ ! -d "node_modules" ]; then npm install; fi

assets/build: $(ASSET_SOURCES) npm
	./node_modules/gulp/bin/gulp.js	

assets/dist: $(ASSET_SOURCES) npm
	NODE_ENV=production ./node_modules/gulp/bin/gulp.js

.PHONY: clean
clean:
	go clean -i ./...
	$(GOPATH)/bin/packr clean
	rm -rf $(EXECUTABLE)

.PHONY: fmt
fmt:
	go fmt $(PACKAGES)

.PHONY: vet
vet:
	go vet $(PACKAGES)

.PHONY: errcheck
errcheck:
	@which errcheck > /dev/null; if [ $$? -ne 0 ]; then \
		go install github.com/kisielk/errcheck@latest; \
	fi
	errcheck $(PACKAGES)

.PHONY: lint
lint:
	@which golint > /dev/null; if [ $$? -ne 0 ]; then \
		go install github.com/golang/lint/golint@latest; \
	fi
	for PKG in $(PACKAGES); do golint -set_exit_status $$PKG || exit 1; done;

.PHONY: test
test:
	for PKG in $(PACKAGES); do go test $$PKG || exit 1; done;

.PHONY: referrer-spam-blacklist
referrer-spam-blacklist:
	wget https://raw.githubusercontent.com/matomo-org/referrer-spam-blacklist/master/spammers.txt -O pkg/aggregator/data/blacklist.txt
	go-bindata -prefix "pkg/aggregator/data/" -o pkg/aggregator/bindata.go -pkg aggregator pkg/aggregator/data/


================================================
FILE: README.md
================================================
Fathom Lite - simple website analytics
==============================
[![Go Report Card](https://goreportcard.com/badge/github.com/usefathom/fathom)](https://goreportcard.com/report/github.com/usefathom/fathom)
[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/usefathom/fathom/master/LICENSE)

Fathom Lite is a previous and open-source version of [Fathom Analytics](https://usefathom.com) (a paid, hosted [Google Analytics alternative](https://usefathom.com/google-analytics-alternative)). It was the very first version of our software, and has been downloaded millions of times!

While we are no longer adding features to this Lite version, we will be continuing to maintain it long-term and fix any bugs that come up.

![Screenshot of the Fathom dashboard](https://github.com/usefathom/fathom/raw/master/assets/src/img/fathom.jpg?v=7)

## Fathom Lite vs Fathom (hosted)

Today’s [Fathom Analytics](https://usefathom.com) is a hosted product with [simple pricing](https://usefathom.com/pricing) based on monthly pageviews. The same core capabilities are included on every plan: for example [API access](https://usefathom.com/api), up to 50 sites, custom events and ecommerce tracking, unlimited email reports and CSV exports, and [forever data retention](https://usefathom.com/features)—with [privacy-law compliance](https://usefathom.com/compliance) and no cookie banner required for analytics.

If you’d rather not run servers or maintenance yourself, try a [30-day free trial](https://usefathom.com/ref/GITHUB) (that link applies a **$10 credit** on your first invoice). Browse [all features](https://usefathom.com/features) or the [live demo](https://app.usefathom.com/demo).

![Screenshot of the Fathom Analytics Dashboard](https://usefathom.com/assets/images/fathom-screenshot.png)

| Feature | Fathom Lite | Fathom (hosted) |
|---------|-------------|-----------------|
| Fully managed | ✗ Self-hosted; you run servers and updates | ✓ Managed for you; [pay per pageviews](https://usefathom.com/pricing) |
| Cookie-free (no analytics banner) | ✗ Uses cookies | ✓ [Cookie-free](https://usefathom.com/features) tracking |
| Current dashboard (real-time, live visitors, filters, details) | ✗ Older Lite UI | ✓ Full dashboard — see [features](https://usefathom.com/features) |
| API | ✗ | ✓ [API](https://usefathom.com/api) on all plans |
| Custom events, ecommerce, UTMs | ✗ | ✓ Events, revenue, campaigns |
| [EU isolation](https://usefathom.com/features/eu-isolation) & [custom domains](https://usefathom.com/features/custom-domains) | ✗ | ✓ EU routing, first-party script domains |
| [GA import](https://usefathom.com/features/ga-importer), email reports, CSV export, [shared dashboards](https://usefathom.com/features) | ✗ | ✓ Unlimited reports & exports |
| Many sites per account | ✗ Typical single install | ✓ Up to 50 sites (more available) |
| Email support & SLA | ✗ Community / as-is | ✓ Support on every plan |
| Global CDN, scaling, backups | ✗ Your responsibility | ✓ Included |
| Active feature development | ✗ Bugfixes / maintenance | ✓ Ongoing — [trial](https://usefathom.com/ref/GITHUB) · [sign up](https://app.usefathom.com/register) |


## Installation


### Production

You can install Fathom on your server by following [our simple instructions](docs/Installation%20instructions.md).

### Development

For getting a development version of Fathom up & running, go through the following steps.

1. Ensure you have [Go](https://golang.org/doc/install#install) and [NPM](https://www.npmjs.com) installed
1. Download the code: `git clone https://github.com/usefathom/fathom.git $GOPATH/src/github.com/usefathom/fathom`
1. Compile the project into an executable: `make build`
1. (Optional) Set [custom configuration values](docs/Configuration.md)
1. (Required) Register a user account: `./fathom user add --email=<email> --password=<password>`
1. Start the webserver: `./fathom server` and then visit **http://localhost:8080** to access your analytics dashboard

## Docker

### Building

Ensure you have Docker installed and run `docker build -t fathom .`.
Run the container with `docker run -d -p 8080:8080 fathom`.

### Running

To run [our pre-built Docker image](https://hub.docker.com/r/usefathom/fathom/), run `docker run -d -p 8080:8080 usefathom/fathom:latest`

## Tracking snippet

To start tracking, create a site in your Fathom dashboard and copy the tracking snippet to the website(s) you want to track.

### Content Security Policy

If you use a [Content Security Policy (CSP)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) to specify security policies for your website, Fathom requires the following CSP directives (replace `yourfathom.com` with the URL to your Fathom instance):

```
script-src: yourfathom.com;
img-src: yourfathom.com;
```

## Copyright and license

MIT licensed. Fathom and Fathom logo are trademarks of Fathom Analytics.


================================================
FILE: assets/src/404.html
================================================
<!DOCTYPE html>
<html class="no-js" lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Not found - Fathom</title> 
  <link href="/assets/css/styles.css" rel="stylesheet">
  <link rel="icon" type="image/png" sizes="32x32" href="/assets/img/favicon.png">
</head>
<body class="404">
   <div class="login-page flex-rapper">
      <div>
         <h1>Page not found</h1>
         <p>Sorry, it seems that the requested page does not exist.</p>
      </div>
   </div>
</body>
</html>


================================================
FILE: assets/src/css/chart.css
================================================
.box-graph {
	background: white;
	margin-left: 0 !important;
}

#chart * { 
	box-sizing: content-box; 
}

#chart .muted { 
	fill: #98a0a6;
}

#chart {
	height: 240px;
}

.bar-pageviews {
	fill: #88ffc6;
}

.bar-visitors {
	fill: #533feb;
}

.axis .domain{ 
	stroke: none; 
}

.axis line {
	stroke: rgba(218, 218, 218, 0.5);
}

.axis text {
	font-size: 12px;
	fill: #98a0a6;
}

.d3-tip {
	font-size: 12px;
	color: #959da5;
	text-align: left;
	background: rgba(0,0,0,.8);
	border-radius: 3px;
}

.tip-heading {
	font-weight: 600;
	padding: 10px;
	line-height: 1;
}

.tip-content {
	display: flex;

}

.tip-content > div {
	padding: 5px 10px;
	width: 50%;
	display: block;
	flex: 1;
	min-width: 90px;
}


.tip-pageviews {
	border-top: 3px solid rgb(136, 255, 198);
}

.tip-visitors {
	border-top: 3px solid rgb(83, 63, 235);
}

.tip-number {
	color: #dfe2e5;
	font-weight: 600;
}


================================================
FILE: assets/src/css/fonts-overpass.css
================================================
@font-face {
	font-family: 'overpass';
	src: url('../fonts/overpass-thin.eot'); /* IE9 Compat Modes */
	src: url('../fonts/overpass-thin.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
	url('../fonts/overpass-thin.woff2') format('woff2'), /* Super Modern Browsers */
	url('../fonts/overpass-thin.woff') format('woff'), /* Pretty Modern Browsers */
	url('../fonts/overpass-thin.ttf')  format('truetype'); /* Safari, Android, iOS */
	font-weight: 200;
	font-style: normal;
}

@font-face {
	font-family: 'overpass';
	src: url('../fonts/overpass-thin-italic.eot');
	src: url('../fonts/overpass-thin-italic.eot?#iefix') format('embedded-opentype'),
	url('../fonts/overpass-thin-italic.woff2') format('woff2'),
	url('../fonts/overpass-thin-italic.woff') format('woff'),
	url('../fonts/overpass-thin-italic.ttf')  format('truetype');
	font-weight: 200;
	font-style: italic;
}


@font-face {
	font-family: 'overpass';
	src: url('../fonts/overpass-extralight.eot');
	src: url('../fonts/overpass-extralight.eot?#iefix') format('embedded-opentype'),
	url('../fonts/overpass-extralight.woff2') format('woff2'),
	url('../fonts/overpass-extralight.woff') format('woff'),
	url('../fonts/overpass-extralight.ttf')  format('truetype');
	font-weight: 300;
	font-style: normal;
}

@font-face {
	font-family: 'overpass';
	src: url('../fonts/overpass-extralight-italic.eot');
	src: url('../fonts/overpass-extralight-italic.eot?#iefix') format('embedded-opentype'),
	url('../fonts/overpass-extralight-italic.woff2') format('woff2'),
	url('../fonts/overpass-extralight-italic.woff') format('woff'),
	url('../fonts/overpass-extralight-italic.ttf')  format('truetype');
	font-weight: 300;
	font-style: italic;
}



@font-face {
	font-family: 'overpass';
	src: url('../fonts/overpass-light.eot');
	src: url('../fonts/overpass-light.eot?#iefix') format('embedded-opentype'),
	url('../fonts/overpass-light.woff2') format('woff2'),
	url('../fonts/overpass-light.woff') format('woff'),
	url('../fonts/overpass-light.ttf')  format('truetype');
	font-weight: 400;
	font-style: normal;
}

@font-face {
	font-family: 'overpass';
	src: url('../fonts/overpass-light-italic.eot');
	src: url('../fonts/overpass-light-italic.eot?#iefix') format('embedded-opentype'),
	url('../fonts/overpass-light-italic.woff2') format('woff2'),
	url('../fonts/overpass-light-italic.woff') format('woff'),
	url('../fonts/overpass-light-italic.ttf')  format('truetype');
	font-weight: 400;
	font-style: italic;
}



@font-face {
	font-family: 'overpass';
	src: url('../fonts/overpass-regular.eot');
	src: url('../fonts/overpass-regular.eot?#iefix') format('embedded-opentype'),
	url('../fonts/overpass-regular.woff2') format('woff2'),
	url('../fonts/overpass-regular.woff') format('woff'),
	url('../fonts/overpass-regular.ttf')  format('truetype');
	font-weight: 500;
	font-style: normal;
}

@font-face {
	font-family: 'overpass';
	src: url('../fonts/overpass-italic.eot');
	src: url('../fonts/overpass-italic.eot?#iefix') format('embedded-opentype'),
	url('../fonts/overpass-italic.woff2') format('woff2'),
	url('../fonts/overpass-italic.woff') format('woff'),
	url('../fonts/overpass-italic.ttf')  format('truetype');
	font-weight: 500;
	font-style: italic;
}




@font-face {
	font-family: 'overpass';
	src: url('../fonts/overpass-semibold.eot');
	src: url('../fonts/overpass-semibold.eot?#iefix') format('embedded-opentype'),
	url('../fonts/overpass-semibold.woff2') format('woff2'),
	url('../fonts/overpass-semibold.woff') format('woff'),
	url('../fonts/overpass-semibold.ttf')  format('truetype');
	font-weight: 600;
	font-style: normal;
}

@font-face {
	font-family: 'overpass';
	src: url('../fonts/overpass-semibold-italic.eot');
	src: url('../fonts/overpass-semibold-italic.eot?#iefix') format('embedded-opentype'),
	url('../fonts/overpass-semibold-italic.woff2') format('woff2'),
	url('../fonts/overpass-semibold-italic.woff') format('woff'),
	url('../fonts/overpass-semibold-italic.ttf')  format('truetype');
	font-weight: 600;
	font-style: italic;
}




@font-face {
	font-family: 'overpass';
	src: url('../fonts/overpass-bold.eot');
	src: url('../fonts/overpass-bold.eot?#iefix') format('embedded-opentype'),
	url('../fonts/overpass-bold.woff2') format('woff2'),
	url('../fonts/overpass-bold.woff') format('woff'),
	url('../fonts/overpass-bold.ttf')  format('truetype');
	font-weight: 700;
	font-style: normal;
}

@font-face {
	font-family: 'overpass';
	src: url('../fonts/overpass-bold-italic.eot');
	src: url('../fonts/overpass-bold-italic.eot?#iefix') format('embedded-opentype'),
	url('../fonts/overpass-bold-italic.woff2') format('woff2'),
	url('../fonts/overpass-bold-italic.woff') format('woff'),
	url('../fonts/overpass-bold-italic.ttf')  format('truetype');
	font-weight: 700;
	font-style: italic;
}



@font-face {
	font-family: 'overpass';
	src: url('../fonts/overpass-extrabold.eot');
	src: url('../fonts/overpass-extrabold.eot?#iefix') format('embedded-opentype'),
	url('../fonts/overpass-extrabold.woff2') format('woff2'),
	url('../fonts/overpass-extrabold.woff') format('woff'),
	url('../fonts/overpass-extrabold.ttf')  format('truetype');
	font-weight: 800;
	font-style: normal;
}

@font-face {
	font-family: 'overpass';
	src: url('../fonts/overpass-extrabold-italic.eot');
	src: url('../fonts/overpass-extrabold-italic.eot?#iefix') format('embedded-opentype'),
	url('../fonts/overpass-extrabold-italic.woff2') format('woff2'),
	url('../fonts/overpass-extrabold-italic.woff') format('woff'),
	url('../fonts/overpass-extrabold-italic.ttf')  format('truetype');
	font-weight: 800;
	font-style: italic;
}


@font-face {
	font-family: 'overpass';
	src: url('../fonts/overpass-heavy.eot');
	src: url('../fonts/overpass-heavy.eot?#iefix') format('embedded-opentype'),
	url('../fonts/overpass-heavy.woff2') format('woff2'),
	url('../fonts/overpass-heavy.woff') format('woff'),
	url('../fonts/overpass-heavy.ttf')  format('truetype');
	font-weight: 900;
	font-style: normal;
}

@font-face {
	font-family: 'overpass';
	src: url('../fonts/overpass-heavy-italic.eot');
	src: url('../fonts/overpass-heavy-italic.eot?#iefix') format('embedded-opentype'),
	url('../fonts/overpass-heavy-italic.woff2') format('woff2'),
	url('../fonts/overpass-heavy-italic.woff') format('woff'),
	url('../fonts/overpass-heavy-italic.ttf')  format('truetype');
	font-weight: 900;
	font-style: italic;
}


================================================
FILE: assets/src/css/pikaday.css
================================================
@charset "UTF-8";

/*!
 * Pikaday
 * Copyright © 2014 David Bushell | BSD & MIT license | http://dbushell.com/
 */

.pika-single {
    z-index: 9999;
    display: block;
    position: relative;
    background: #fff; 
    border-radius: 4px; 
    box-shadow: 0 2px 8px 0 rgba(70,73,77,.16);
}

.pika-single:before,
.pika-single:after {
    content: " ";
    display: table;
}
.pika-single:after { clear: both }
.pika-single { *zoom: 1 }

.pika-single.is-hidden {
    display: none;
}

.pika-single.is-bound {
    position: absolute;
}

.pika-lendar {
    float: left;
    width: 240px;
    margin: 8px;
}

.pika-title {
    position: relative;
    text-align: center;
}

.pika-label {
    display: inline-block;
    *display: inline;
    position: relative;
    z-index: 9999;
    overflow: hidden;
    margin: 0;
    padding: 5px 3px;
    font-size: 14px;
    line-height: 20px;
    font-weight: 500;
}
.pika-title select {
    cursor: pointer;
    position: absolute;
    z-index: 9998;
    margin: 0;
    left: 0;
    top: 5px;
    filter: alpha(opacity=0);
    opacity: 0;
}

.pika-prev,
.pika-next {
    display: block;
    cursor: pointer;
    position: relative;
    outline: none;
    border: 0;
    padding: 0;
    width: 20px;
    height: 30px;
    text-indent: 20px;
    white-space: nowrap;
    overflow: hidden;
    background-color: transparent;
    background-position: center center;
    background-repeat: no-repeat;
    background-size: 75% 75%;
    opacity: .5;
    *position: absolute;
    *top: 0;
}

.pika-prev:hover,
.pika-next:hover {
    opacity: 1;
}

.pika-prev,
.is-rtl .pika-next {
    float: left;
    background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAYAAAAsEj5rAAAAUklEQVR42u3VMQoAIBADQf8Pgj+OD9hG2CtONJB2ymQkKe0HbwAP0xucDiQWARITIDEBEnMgMQ8S8+AqBIl6kKgHiXqQqAeJepBo/z38J/U0uAHlaBkBl9I4GwAAAABJRU5ErkJggg==');
    *left: 0;
}

.pika-next,
.is-rtl .pika-prev {
    float: right;
    background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAYAAAAsEj5rAAAAU0lEQVR42u3VOwoAMAgE0dwfAnNjU26bYkBCFGwfiL9VVWoO+BJ4Gf3gtsEKKoFBNTCoCAYVwaAiGNQGMUHMkjGbgjk2mIONuXo0nC8XnCf1JXgArVIZAQh5TKYAAAAASUVORK5CYII=');
    *right: 0;
}

.pika-prev.is-disabled,
.pika-next.is-disabled {
    cursor: default;
    opacity: .2;
}

.pika-select {
    display: inline-block;
    *display: inline;
}

.pika-table {
    width: 100%;
    border-collapse: collapse;
    border-spacing: 0;
    border: 0;
}

.pika-table th,
.pika-table td {
    width: 14.285714285714286%;
    padding: 0;
}

.pika-table th {
    color: #98a0a6;
    font-size: 12px;
    line-height: 25px;
    font-weight: 500;
    text-align: center;
}

.pika-button {
    cursor: pointer;
    display: block;
    box-sizing: border-box;
    -moz-box-sizing: border-box;
    outline: none;
    border: 0;
    margin: 0;
    width: 100%;
    padding: 4px;
    color: #666;
    font-size: 12px;
    line-height: 16px;
    text-align: right;
    background: #f5f7fa;
}

.pika-week {
    font-size: 11px;
    color: #98a0a6;
}

.is-today .pika-button {
    color: #533feb;
    font-weight: 500;
}

.is-selected .pika-button,
.has-event .pika-button {
    color: #fff;
    font-weight: 500;
    background: #46494d;
}

.has-event .pika-button {
    background: #005da9;
}

.is-disabled .pika-button,
.is-inrange .pika-button {
    background: #88ffc6;
}

.is-startrange .pika-button {
    background: #88ffc6;
    box-shadow: none;
}

.is-endrange .pika-button {
    color: #fff;
    background: #33aaff;
    box-shadow: none;
}

.is-disabled .pika-button {
    pointer-events: none;
    cursor: default;
}

.is-outside-current-month .pika-button {
}

.is-selection-disabled {
    pointer-events: none;
    cursor: default;
}

.pika-button:hover,
.pika-row.pick-whole-week:hover .pika-button {
    color: #46494d;
    background: #88ffc6;
    box-shadow: none;
}

.pika-table abbr {
    border: none;
    cursor: normal;
    text-decoration: none;
        font-size: 12px; 
}



================================================
FILE: assets/src/css/styles.css
================================================
/*
overpass    200, 500, 600
purple      #533feb
green       #88ffc6

dark        #46494d
medium      #98a0a6
light       #f5f7fa

padding     8, 16, 20, 32, 64, 128, 256, 512, 1024
font size   12, 16, 64
 */

::selection       { background: #a0ffd1; }
::-moz-selection  { background: #a0ffd1; }
* { margin: 0; padding: 0; border: none; outline: none; list-style: none; box-sizing: border-box; text-decoration: none; font-size: 100%; vertical-align: baseline; line-height: 1.2; }

html, body, #root { height: 100%;  background: #f5f7fa; }
body { overflow-y: scroll;  font: 400 16px "overpass", sans-serif; color: #222; text-align: center; padding: 8px; }

.rapper { text-align: left; margin: 0 auto; max-width: 1124px; }

header, footer { margin: 16px 0; }
section {}
footer { font-size: 12px; color: #aaa; }

a { transition: ease color .2s; }

nav { position: relative; font-size: 12px; }
nav a { color: #222; }
footer nav a { color: #aaa; padding: 4px 0; display: inline-block; }
footer nav a:hover { color: #222; }

header li { padding: 8px 0; }
nav li.logo a { color: #533feb; font-size: 16px; }
nav li.logo a:hover { color: #222; }

nav li.sites { background: #fff url("data:image/svg+xml;charset=utf8,%3Csvg aria-hidden='true' role='img' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 512'%3E%3Cpath fill='%23533feb' d='M151.5 347.8L3.5 201c-4.7-4.7-4.7-12.3 0-17l19.8-19.8c4.7-4.7 12.3-4.7 17 0L160 282.7l119.7-118.5c4.7-4.7 12.3-4.7 17 0l19.8 19.8c4.7 4.7 4.7 12.3 0 17l-148 146.8c-4.7 4.7-12.3 4.7-17 0z'%3E%3C/path%3E%3C/svg%3E") 97% 8px no-repeat; border-radius: 4px; padding: 8px; background-size: 10px auto; }
nav li.sites.expanded { background: #533feb url("data:image/svg+xml;charset=utf8,%3Csvg aria-hidden='true' role='img' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 512'%3E%3Cpath fill='%23fff' d='M168.5 164.2l148 146.8c4.7 4.7 4.7 12.3 0 17l-19.8 19.8c-4.7 4.7-12.3 4.7-17 0L160 229.3 40.3 347.8c-4.7 4.7-12.3 4.7-17 0L3.5 328c-4.7-4.7-4.7-12.3 0-17l148-146.8c4.7-4.7 12.3-4.7 17 0z'%3E%3C/path%3E%3C/svg%3E") 97% 8px no-repeat; background-size: 10px auto; }
nav li ul { display: none; position: absolute; z-index: 1001; width: 100%; background: #533feb; border-radius: 4px; padding: 8px 0; margin: 0 0 0 -8px; }
nav li.expanded ul { display: block; box-shadow: 0 2px 8px 0 rgba(34,34,34,.10); }
nav li.sites.expanded a { color: #fff; }
nav li ul li { padding: 0 4px; width: 100%; }
nav li ul a { color: #ddd; display: inline-block; width: 100%; font-size: 13px; padding: 4px; }
nav li ul a:hover { background: rgba(255,255,255,.2); border-radius: 2px; }
nav li ul li.add-new a { color: #88ffc6 !important; }

nav .settings svg { width: 16px; display: inline-block; transition: ease all .2s; }
nav .settings svg path { fill: #533feb; }
nav .settings svg:hover { transform: rotate(45deg); }			

nav.date-nav { margin: 32px 0 8px; }
nav.date-nav ul { margin: 0 8px 0 0; border-radius: 4px; background: #e8ecee; padding: 8px 8px 8px 16px; display: inline-block; }
nav.date-nav li { display: inline-block; padding: 0; }
nav.date-nav li a,
nav.date-nav li input { margin-right: 8px; color: #98a0a6; }
nav.date-nav li input { background: transparent; border: 0; font-size: inherit; padding: 0; cursor: pointer; display: inline-block; width: 9.8ch; min-width: 60px; text-align: center; }
nav.date-nav li.current a { color: #533feb; }
nav.date-nav li a:hover { color: #2b2d2f; }
nav.date-nav li span { margin-right: 8px; }

/*
.date-nav { margin-bottom: 12px; }
.date-nav li a { position: relative; font-size: 12px; text-transform: uppercase; padding-right: 8px; }
.date-nav li.custom { color: #aaa; float: right; margin: 0; }
.date-nav li.custom input { 
	display: inline-block;
	width: 75px;
	border: 0;
	font-size: inherit;
	background: transparent;
	padding: 0;
	cursor: pointer; 
}
.date-nav li a:hover { color: #aaa; }
.date-nav li.active a { padding-right: 8px; z-index: 1; color: #222; }
.date-nav li.active a:after { content:""; background: #88ffc6; display: block; width: 100%; height: 3px; position: absolute; top: 6px; z-index: -1; margin: 0 0 0 -4px; }
*/
.box { background: #fff; border-radius: 4px; margin-bottom: 16px; padding: 16px; box-shadow: 0 2px 8px 0 rgba(34,34,34,.10); }

.box-totals { background: #222; color: #ddd; }

.totals-detail { display: grid; grid-template-columns: 1fr 1.6fr; grid-gap: 12px; }
.total-numbers { text-align: right; }
.current-detail div { color: #88ffc6; }

.table-row { display: grid; grid-template-columns: 4fr 1fr 1fr; grid-gap: 12px; padding: 8px 0; position: relative; }
.table-row.header { font-size: 12px; text-transform: uppercase; color: #aaa; }
.table-row a { color: #222; }
.table-row a:hover { color: #533feb; }
.cell { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; text-align: right; z-index: 1; position: relative; }
.cell.main-col { text-align: left; }

.table-row:after {  content: ""; background: #88ffc6; position: absolute; height: 34px; top: 0; left: -16px; opacity: .2; border-right: 2px solid #45ce8c; z-index: 0; } 
.table-row.header:after { background: none; border: none; }

.row.pag {
	margin-top: 16px;
	grid-template-columns: 1fr 1fr;
	grid-gap: 4px;
}
.row.pag a {
    color: #98a0a6;
    font-size: 19px;
}

.modal-wrap { position: fixed; height: 100%; width: 100%; top: 0; left: 0; z-index: 1977; background: rgba(20,20,20,.8); display: grid; grid-template-columns: 1fr; align-items: center; }
.modal { max-width: 480px; width: 100%; margin: 0 auto; text-align: left; background: #fff; z-index: 1978; height: auto; border-radius: 4px; box-shadow: 0 2px 8px 0 rgba(34,34,34,.10); overflow: hidden; }
.modal p { padding: 16px; font-size: 12px; color: #aaa; }

small { font-size: 12px; color: #aaa; }
small a { color: #aaa; }

.modal form { padding: 0 16px; }
label { padding: 4px 0; display: block; }
fieldset { display: block; width: 100%; margin-bottom: 32px; }
fieldset:last-child { margin-bottom: 16px; }
input, textarea { background: #f5f7fa; padding: 4px; width: 100%; display: block; font: 400 16px "overpass", sans-serif; color: #222; border-bottom: 2px solid #e6e8eb; }
textarea { font-size: 12px; min-height: 120px; resize: none; overflow-y: scroll; }
button { background: #88ffc6; color: #222; padding: 4px 16px; font: 400 16px "overpass", sans-serif; cursor: pointer; border-radius: 4px; }
div.delete a { color: red; }

.w100:after{width:99%}.w99:after{width:99%}.w98:after{width:98%}.w97:after{width:97%}.w96:after{width:96%}.w95:after{width:95%}.w94:after{width:94%}.w93:after{width:93%}.w92:after{width:92%}.w91:after{width:91%}.w90:after{width:90%}.w89:after{width:89%}.w88:after{width:88%}.w87:after{width:87%}.w86:after{width:86%}.w85:after{width:85%}.w84:after{width:84%}.w83:after{width:83%}.w82:after{width:82%}.w81:after{width:81%}.w80:after{width:80%}.w79:after{width:79%}.w78:after{width:78%}.w77:after{width:77%}.w76:after{width:76%}.w75:after{width:75%}.w74:after{width:74%}.w73:after{width:73%}.w72:after{width:72%}.w71:after{width:71%}.w70:after{width:70%}.w69:after{width:69%}.w68:after{width:68%}.w67:after{width:67%}.w66:after{width:66%}.w65:after{width:65%}.w64:after{width:64%}.w63:after{width:63%}.w62:after{width:62%}.w61:after{width:61%}.w60:after{width:60%}.w59:after{width:59%}.w58:after{width:58%}.w57:after{width:57%}.w56:after{width:56%}.w55:after{width:55%}.w54:after{width:54%}.w53:after{width:53%}.w52:after{width:52%}.w51:after{width:51%}.w50:after{width:50%}.w49:after{width:49%}.w48:after{width:48%}.w47:after{width:47%}.w46:after{width:46%}.w45:after{width:45%}.w44:after{width:44%}.w43:after{width:43%}.w42:after{width:42%}.w41:after{width:41%}.w40:after{width:40%}.w39:after{width:39%}.w38:after{width:38%}.w37:after{width:37%}.w36:after{width:36%}.w35:after{width:35%}.w34:after{width:34%}.w33:after{width:33%}.w32:after{width:32%}.w31:after{width:31%}.w30:after{width:30%}.w29:after{width:29%}.w28:after{width:28%}.w27:after{width:27%}.w26:after{width:26%}.w25:after{width:25%}.w24:after{width:24%}.w23:after{width:23%}.w22:after{width:22%}.w21:after{width:21%}.w20:after{width:20%}.w19:after{width:19%}.w18:after{width:18%}.w17:after{width:17%}.w16:after{width:16%}.w15:after{width:15%}.w14:after{width:14%}.w13:after{width:13%}.w12:after{width:12%}.w11:after{width:11%}.w10:after{width:10%}.w09:after{width:9%}.w08:after{width:8%}.w07:after{width:7%}.w06:after{width:6%}.w05:after{width:5%}.w04:after{width:4%}.w03:after{width:3%}.w02:after{width:2%}.w01:after{width:1%}.w00:after{width:0}	



@media ( min-width: 1220px ) {
	body { padding: 0; }
	header, footer { margin: 32px 0; }

	.main-nav li,
	footer li {
		display: inline-block;
		margin-right: 16px;
	}

	nav li.sites { display: inline-block; width: 204px; margin-right: 0; }
	nav li.sites, nav li.settings { float: right; }
	nav li.sites, nav li.sites.expanded { background-position: 184px 8px; }

	nav .date-nav li { margin-right: 8px; }
	nav li ul { width: 204px; right: 0; margin: 0;  }

	.box { margin: 0; padding: 32px 16px; }
	.boxes { display: grid; grid-template-columns: 276px 420px 420px; grid-gap: 4px; }
	.box-totals { grid-column: 1; grid-row: 1/3; }
	.box-graph { grid-column: 2/4; grid-row: 1;  margin: 0 0 4px 0; }
	.box-pages { grid-column: 2; grid-row: 2 ; }
	.box-referrers { grid-column: 3; grid-row: 2; }

	.half { display: grid; grid-template-columns: 1fr 1fr; grid-gap: 12px; align-items: center; }
	.half div { text-align: right; }
	.half div.submit { text-align: left; }

	.totals-detail { display: block; margin-bottom: 32px; }
	.total-heading { font-size: 12px; text-transform: uppercase; color: #888; line-height: .8; }
	.total-numbers { font-size: 68px; font-weight: 200; letter-spacing: -.06em; text-align: left; }

}


.login-page.flex-rapper { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
.login-rapper { text-align: left; width: 320px; }
.login-page label { position: relative; }
.login-page  label svg { width: 16px;  height: auto;  fill: #533feb; position: absolute; left: -28px; top: 12px; }
.login-page  input { border-radius: 0; border: none; outline: none; background: #fff; box-sizing: border-box; width: 100%; padding: 8px; margin-bottom: 4px; display: block; }
.login-page  input{ line-height: 1; font: 400 16px/1 'overpass', sans-serif; }

.login-page  button { width: 100%; box-sizing: border-box; padding: 8px; background: #533feb; color: #fff; border: 0; font: 400 16px/1 'overpass', sans-serif; }

.login-page  small { color: #98a0a6; font-size: 14px; margin: 48px auto 0 auto; text-align: center; display: block; }
.login-page  small a { color: #98a0a6; font-size: 14px; display: inline-block; padding: 0 8px; }


.notice { background: #ecedf4; border-radius: 4px; padding: 16px; text-align: left; color: color: #222; font-size: 14px; display: grid; grid-template-columns: 1fr min-content; margin: 32px 0; gap: 32px; align-items: center; }
.notice a { color: #aaa; }
.notice .notice-text a { color: #533feb; text-decoration: underline; }


================================================
FILE: assets/src/css/util.css
================================================
.small-margin {
	margin-top: 20px;
	margin-bottom: 20px;
}

.cf:after {
	content: "";
	display: table;
	clear: both;
}

.ac {
    text-transform: uppercase;
}

.sm {
    font-size: 11px;
    font-weight: 500;
    color: #98a0a6;
}

@media(max-width: 600px) {
	.hide-on-mobile { display: none !important; }
}

.right {
	float: right;
}

.left {
	float: left;
}

.notification {
	position: fixed;
	top: 20px;
	left: 0; right: 0;
	text-align: center;
	width: 100%;
}

.notification .notification-error {
	padding: 4px;
	display: inline-block;
	background-color: #f2dede;
	border: 1px solid #ebccd1;
}

@keyframes fadeInUp {
	0% { opacity: 0; transform: translateY(20px); }
	100% { opacity: 1; transform: translateY(0);  }
}

@keyframes fadeInDown {
	0% { opacity: 0; transform: translateY(-20px); }
	100% { opacity: 1; transform: translateY(0);  }
}

.animated { animation-duration: .4s; animation-fill-mode: both; }

.delayed_02s { animation-delay: .2s; }
.delayed_03s { animation-delay: .3s; }
.delayed_04s { animation-delay: .4s; }
.delayed_05s { animation-delay: .5s; }
.delayed_06s { animation-delay: .6s; }

.fadeInUp { animation-name: fadeInUp; }
.fadeInDown { animation-name: fadeInDown; }

.loading {
	opacity: 0.6;
}


================================================
FILE: assets/src/index.html
================================================
<!DOCTYPE html>
<html class="no-js" lang="en">
<head>
  <title>Fathom - simple website analytics</title>
  <link href="assets/css/styles.css" rel="stylesheet">
  <meta charset="utf-8">
  <meta name="referrer" content="no-referrer">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="icon" type="image/png" sizes="32x32" href="assets/img/favicon.png">
</head>
<body class="fathom">
  <div id="root"></div>
  <noscript>To use Fathom, please enable JavaScript.</noscript> 
  <script>
    document.documentElement.className = document.documentElement.className.replace('no-js', '');
  </script>
  <script src="assets/js/script.js"></script>
</body>
</html>


================================================
FILE: assets/src/js/components/Chart.js
================================================
'use strict';

import { h, Component } from 'preact';
import Client from '../lib/client.js';
import { bind } from 'decko';
import * as numbers from '../lib/numbers.js';
import * as d3 from 'd3';
import 'd3-transition';
d3.tip = require('d3-tip');

const formatMonth = d3.timeFormat("%b"),
  formatMonthDay = d3.timeFormat("%b %e");

const t = d3.transition().duration(600).ease(d3.easeQuadOut);

function xTickFormat(tickStep, n) {
  let formatters = {
    hour: (d, i) => {
      if(i === 0 || i === n-1) {
        return formatMonthDay(d);
      }

      if(n <= 24 && d.getHours() === 0 || d.getHours() === 12) {
        return d.getHours() + ":00";
      }     

      return '';
    },

    day: (d, i, o) => {
      if( i === 0 || i === n-1) {
       return formatMonthDay(d);
      }

      return '';
    },
    month: (d, i) => {
      if(n>28) {
        return d.getFullYear();
      }

      return d.getMonth() === 0 ? d.getFullYear() : formatMonth(d);
    }
  }

  return formatters[tickStep];
}


const incrementers = {
  'hour': d => d.setHours(d.getHours() + 1),
  'day': d => d.setDate(d.getDate() + 1),
  'month': d => d.setMonth(d.getMonth() + 1)
}

function incrementDate(date, incr) {
  return incrementers[incr](date);
}

class Chart extends Component {
  constructor(props) {
    super(props)

    this.state = {
      loading: false,
      data: [],
      chartData: [],
      diffInDays: 1,
    }
  }

  componentWillReceiveProps(newProps) {
    let daysDiff = Math.round((newProps.dateRange[1]-newProps.dateRange[0])/1000/24/60/60);

    this.setState({
      diffInDays: daysDiff,
      tickStep: newProps.tickStep,
    })

    if( newProps.siteId != this.props.siteId || newProps.dateRange[0] != this.props.dateRange[0] || newProps.dateRange[1] != this.props.dateRange[1] ) {
      this.fetchData(newProps)
    } else if (newProps.tickStep != this.props.tickStep) {
      this.chartData()
      this.redrawChart()
    }
  }

  @bind
  chartData() {
    let startDate = new Date(this.props.dateRange[0]);
    let endDate = this.props.dateRange[1];
    let newData = [];

    // if grouping by month, fix date to 1st of month
    if(this.state.tickStep === 'month') {
      startDate.setDate(1);
    }

    // instantiate JS Date objects
    let data = this.state.data.map(d => {
      d.Date = new Date(d.Date);
      return d
    })
  
    // make sure we have values for each date (so 0 value for gaps)
    let currentDate = startDate, nextDate, tick, offset = 0;
    while(currentDate < endDate) {
      tick = {
          "Pageviews": 0,
          "Visitors": 0,
          "Date": new Date(currentDate),
      };

      nextDate = new Date(currentDate)
      nextDate = incrementDate(nextDate, this.state.tickStep)

      // grab data that falls between currentDate & nextDate
      for(let i=data.length-offset-1; i>=0; i--) {
        // Because 9AM should be included in 9AM-10AM range, check for equality here
        if( data[i].Date >= nextDate) {
          break;
        }

         // increment offset so subsequent dates can skip first X items in array
         offset += 1;

        // continue to next item in array if we're still below our target date
        if( data[i].Date < currentDate) {
          continue;
        }

        // add to tick data
        tick.Pageviews += data[i].Pageviews;
        tick.Visitors += data[i].Visitors;
      }

      newData.push(tick);  
      currentDate = nextDate;
    }

    this.setState({
      chartData: newData,
    })
  }
  
  @bind
  prepareChart() {
    let padding = { top: 12, right: 12, bottom: 24, left: 40 };
    let height = 240;
    let width = this.base.clientWidth;

    this.innerWidth = width - padding.left - padding.right;
    this.innerHeight = height - padding.top - padding.bottom;

    this.ctx =  d3.select(this.base)
      .append('svg')
      .attr('width', width)
      .attr('height', height)
      .append('g')
      .attr('transform', 'translate(' + padding.left + ', '+padding.top+')')

    this.x = d3.scaleBand().range([0, this.innerWidth]).padding(0.1)
    this.y = d3.scaleLinear().range([this.innerHeight, 0])

      // tooltip
    this.tip = d3.tip().attr('class', 'd3-tip').html((d) => {
      let title;

      if(this.state.tickStep === 'hour') {
        title = `${d.Date.toLocaleDateString()} ${d.Date.getHours()}:00 - ${d.Date.getHours() + 1}:00`
      } else if(this.state.tickStep === 'day' ) {
        title = `${d.Date.toLocaleDateString()} (${d3.timeFormat("%a")(d.Date)})`
      } else {
        title = d3.timeFormat("%B %Y")(d.Date)
      }

      return (`
      <div class="tip-heading">${title}</div>
      <div class="tip-content">
        <div class="tip-pageviews">
          <div class="tip-number">${numbers.formatPretty(d.Pageviews)}</div>
          <div class="tip-metric">Pageviews</div>
        </div>
        <div class="tip-visitors">
          <div class="tip-number">${numbers.formatPretty(d.Visitors)}</div>
          <div class="tip-metric">Visitors</div>
        </div>
      </div>`
      )});

    this.ctx.call(this.tip)
  }

  @bind
  redrawChart() {
    let data = this.state.chartData;

    if( ! this.ctx ) {
      this.prepareChart()
    }

    let graph = this.ctx;
    let innerWidth = this.innerWidth
    let innerHeight = this.innerHeight
    const max = d3.max(data, d => d.Pageviews); 
    let x = this.x.domain(data.map(d => d.Date))
    let y = this.y.domain([0, max*1.1])
    let yAxis = d3.axisLeft().scale(y).ticks(3).tickSize(-innerWidth).tickFormat(v => numbers.formatPretty(v))
    let xAxis = d3.axisBottom().scale(x).tickFormat(xTickFormat(this.state.tickStep, data.length))

    // only show first and last tick if we have more than 28 ticks to show
    if(data.length > 28) {
      let tickValues = data.map(d => d.Date).filter((d, i) => i === 0 || i === data.length-1);
      xAxis.tickValues(tickValues)
      xAxis.tickFormat(xTickFormat(this.state.tickStep, tickValues.length))
    }

    // empty previous graph
    graph.selectAll('*').remove()

    // add text indicating there's no data yet
    if( max === 0 ) {
      graph.append('text')
        .attr('class', 'muted')
        .attr("text-anchor", "middle")
        .attr('x', innerWidth / 2 - 30)
        .attr('y', innerHeight / 2)
        .text('Nothing here, yet.')
    }

    // add axes
    let yTicks = graph.append("g")
      .attr("class", "y axis")
      .call(yAxis);

    let xTicks = graph.append("g")
      .attr("class", "x axis")
      .attr('transform', 'translate(0,' + innerHeight + ')')
      .call(xAxis)
    
    // add data for each tick that we have something to show for
    let barWidth = x.bandwidth()
    let ticks = graph.selectAll('.item')
      .data(data.filter(d => d.Pageviews > 0 || d.Visitors > 0)).enter()
      .append('g')
      .attr('class', 'item') 
      
    let pageviews = ticks.append('rect')
      .attr('class', 'bar-pageviews') 
      .attr('x', d => x(d.Date))
      .attr('width', barWidth)
      .attr("y", innerHeight)
      .attr("height", 0)
      
    let visitors = ticks.append('rect')
      .attr('class', 'bar-visitors')
      .attr('x', d => x(d.Date) )
      .attr('width', barWidth)
      .attr("y", innerHeight)
      .attr("height", 0)
    
    pageviews.transition(t)
      .attr('y', d => y(d.Pageviews))
      .attr('height', (d) => innerHeight - y(d.Pageviews)) 

    visitors.transition(t)
      .attr('height', (d) => (innerHeight - y(d.Visitors)) )
      .attr('y', (d) => y(d.Visitors))   
      
    // add event listeners for tooltips
    ticks.on('mouseover', this.tip.show).on('mouseout', this.tip.hide)   
  }

  @bind
  fetchData(props) {
    this.setState({ loading: true })

    let before = props.dateRange[1]/1000;
    let after = props.dateRange[0]/1000;

    Client.request(`/sites/${props.siteId}/stats/site?before=${before}&after=${after}`)
      .then(data => { 

        this.setState({ 
          loading: false,
          data: data,
        })

        this.chartData()
        this.redrawChart()
      })
  }
 
  render(props, state) {
    return (
       <div id="chart" class={state.loading ? 'loading': ''}></div>
    )
  }
}

export default Chart


================================================
FILE: assets/src/js/components/CountWidget.js
================================================
'use strict';

import { h, Component } from 'preact';
import * as numbers from '../lib/numbers.js';
import { bind } from 'decko';
import classNames from 'classnames';

const duration = 600;
const easeOutQuint = function (t) { return 1+(--t)*t*t*t*t };

class CountWidget extends Component {
  componentWillReceiveProps(newProps, newState) {
    if(newProps.value == this.props.value) {
      return;
    }

    this.countUp(this.props.value || 0, newProps.value);
  }

  // TODO: Move to component of its own
  @bind
  countUp(fromValue, toValue) {
    const format = this.formatValue.bind(this);
    const startValue = isFinite(fromValue) ? fromValue : 0;
    const numberEl = this.numberEl;
    const diff = toValue - startValue;
    let startTime = performance.now();

    const tick = function(t) {
      let progress = Math.min(( t - startTime ) / duration, 1);
      let newValue = startValue + (easeOutQuint(progress) * diff);
      numberEl.textContent = format(newValue)

      if(progress < 1) {
        window.requestAnimationFrame(tick);
      }
    }

    window.requestAnimationFrame(tick);
  }

  @bind
  formatValue(value) {
    let formattedValue = "-";

    if(isFinite(value)) {
      switch(this.props.format) {
        case "percentage":
          formattedValue = numbers.formatPercentage(value)
        break;

        default:
        case "number":
          formattedValue = numbers.formatPretty(Math.round(value))
        break;

        case "duration":
          formattedValue = numbers.formatDuration(value)
        break;
      }
    }

    return formattedValue;
  }

  render(props, state) {
    return (
       <div class={classNames("totals-detail", { loading: props.loading })}>
        <div class="total-heading">{props.title}</div>
        <div class="total-numbers" ref={(e) => { this.numberEl = e; }}>{this.formatValue(props.value)}</div>
      </div>
    )
  }
}

export default CountWidget


================================================
FILE: assets/src/js/components/DatePicker.js
================================================
'use strict';

import { h, Component } from 'preact';
import { bind } from 'decko';
import Pikadayer from './Pikadayer.js';
import classNames from 'classnames';
import {hashParams} from "../lib/util";

const padZero = (n) => n < 10 ? '0'+n : ''+n;

let now = new Date();
window.setInterval(() => {
  now = new Date();
}, 60000 );

const availablePeriods = {
  '1d': {
    label: '1d',
    start: function() {
      return new Date(now.getFullYear(), now.getMonth(), now.getDate());
    },
    end: function() {
      return this.start();
    },
  },
  '1w': {
    label: '1w',
    start: function() {
      return new Date(now.getFullYear(), now.getMonth(), now.getDate()-6);
    },
    end: function() {
      return new Date(now.getFullYear(), now.getMonth(), now.getDate());
    },
 },
 '4w': {
    label: '4w',
    start: function() {
      return new Date(now.getFullYear(), now.getMonth(), now.getDate()-4*7+1);
    },
    end: function() {
      return new Date(now.getFullYear(), now.getMonth(), now.getDate());
    },
 },
 'mtd': {
    label: 'Mtd',
    start: function() {
      return new Date(now.getFullYear(),  now.getMonth(), 1);
    },
    end: function() {
      return new Date(now.getFullYear(), now.getMonth()+1, 0);
    },
 },
'qtd': {
  label: 'Qtd',
  start: function() {
    let qs = Math.ceil((now.getMonth()+1) / 3) * 3 - 3;
    return new Date(now.getFullYear(), qs, 1);

  },
  end: function() {
    let start = this.start();
    return new Date(start.getFullYear(), start.getMonth() + 3, 0);
  },
 },
 'ytd': {
  label: 'Ytd',
  start: function() {
    return new Date(now.getFullYear(), 0, 1);
  },
  end: function() {
    return new Date(now.getFullYear()+1, 0, 0);
  },
 },
 'all': {
  label: 'All',
  start: function() {
    return new Date(2018, 6, 1);
  },
  end: function() {
    return new Date();
  },
 }
}

class DatePicker extends Component {
  constructor(props) {
    super(props)

    let params = hashParams();

    this.state = {
      period: params.p || window.localStorage.getItem('period') || '1w',
      startDate: new Date(params.s || 'now'),
      endDate: new Date(params.e || 'now'),
      groupBy: params.g || 'day',
      site: params.site || 1
    }    
    this.state.diff = this.calculateDiff(this.state.startDate, this.state.endDate)

    if(this.state.period !== 'custom') {
      this.updateDatesFromPeriod(this.state.period, params.g)
    } else {
      this.props.onChange(this.state);
    }
  }

  componentDidMount() {
    window.addEventListener('keydown', this.handleKeyPress);
  }

  componentWillUnmount() {
    window.removeEventListener('keydown', this.handleKeyPress)
  }

  @bind
  updateDatesFromPeriod(period, groupBy) {
    if(typeof(availablePeriods[period]) !== "object") {
      period = "1w";
    }
    let p = availablePeriods[period];
    this.setDateRange(p.start(), p.end(), period, groupBy);
  }

  @bind
  setDateRange(start, end, period, groupBy) {
    // don't update state if start > end. user may be busy picking dates.
    if(start > end) {
      return;
    }

    // include start & end day by forcing time
    start.setHours(0, 0, 0);
    end.setHours(23, 59, 59);

    let diff =  this.calculateDiff(start, end)
    if(!groupBy) {
      groupBy = 'day';

      if(diff >= 31) {
        groupBy = 'month';
      } else if( diff < 2) {
        groupBy = 'hour';
      }
    }
   
   
    this.setState({
      period: period,
      startDate: start,
      endDate: end,
      diff: diff,
      groupBy: groupBy,
    });

    // use slight delay for updating rest of application to allow this function to be called again
    if(!this.timeout) {
      this.timeout = window.setTimeout(() => {
        this.props.onChange(this.state);
        this.updateURL()
        this.timeout = null;
      }, 5)
    }
  }

  calculateDiff(start, end) {
    return Math.round((end - start) / 1000 / 60 / 60 / 24)
  }

  updateURL() {
    if(this.state.period !== 'custom') {
      window.history.replaceState(this.state, null, `#!p=${this.state.period}&g=${this.state.groupBy}&site=${this.state.site}`)
    } else {
      window.history.replaceState(this.state, null, `#!p=custom&s=${encodeURIComponent(this.state.startDate.toISOString())}&e=${encodeURIComponent(this.state.endDate.toISOString())}&g=${this.state.groupBy}&site=${this.state.site}`)
    }
  }

  @bind
  setPeriod(e) {
    e.preventDefault();

    let newPeriod = e.target.getAttribute('data-value');
    if( newPeriod === this.state.period) {
      return;
    }

    window.localStorage.setItem('period', this.state.period)
    this.updateDatesFromPeriod(newPeriod);
  }

  dateValue(date) {
    return date.getFullYear() + '-' + padZero(date.getMonth() + 1) + '-' + padZero(date.getDate());
  }

  @bind
  setStartDate(date) {
    this.setDateRange(date, this.state.endDate, 'custom')
  }

  @bind
  setEndDate(date) {
    this.setDateRange(this.state.startDate, date, 'custom')
  }

  @bind
  handleKeyPress(evt) {
    // Don't handle input when the user is in a text field or text area.
    let tag = evt.target.tagName;
    if(tag === "INPUT" || tag === "TEXTAREA") {
      return;
    }

    // TODO: Account for leap years
    let diff = this.state.endDate - this.state.startDate + 1000;
    let newStartDate, newEndDate;

    switch(evt.which) {
      // left-arrow
      case 37:
        newStartDate = new Date(+this.state.startDate - diff)
        newEndDate = new Date(+this.state.endDate - diff)
        this.setDateRange(newStartDate, newEndDate)
      break;

      //right-arrow
      case 39:
      newStartDate = new Date(+this.state.startDate + diff)
      newEndDate = new Date(+this.state.endDate + diff)
      this.setDateRange(newStartDate, newEndDate)
      break;
    }
  }

  @bind
  setGroupBy(e) {
    this.setState({
      groupBy: e.target.getAttribute('data-value')
    })
    this.props.onChange(this.state);
    this.updateURL()
  }

  render(props, state) {
    const presets = Object.keys(availablePeriods).map((id) => {
      let p = availablePeriods[id];
      return (
        <li class={classNames({ current: id == state.period })}>
          <a href="javascript:void(0);" data-value={id} onClick={this.setPeriod}>{p.label}</a>
        </li>
      );
    });

    return (
      <nav class="date-nav sm ac">
        <ul>
          {presets}
        </ul>
        <ul>
          <li><Pikadayer value={this.dateValue(state.startDate)} onSelect={this.setStartDate} /> <span>›</span> <Pikadayer value={this.dateValue(state.endDate)} onSelect={this.setEndDate}  /></li>
        </ul>
        <ul>
         {state.diff < 31 ? (<li class={classNames({ current: 'hour' === state.groupBy })}><a href="javascript:;" data-value="hour" onClick={this.setGroupBy}>Hourly</a></li>) : ''}
         <li class={classNames({ current: 'day' === state.groupBy })}><a href="javascript:;" data-value="day" onClick={this.setGroupBy}>Daily</a></li>
         {state.diff >= 31 ? (<li class={classNames({ current: 'month' === state.groupBy })}><a href="javascript:;" data-value="month" onClick={this.setGroupBy}>Monthly</a></li>) : ''}
        </ul>
      </nav>
    )

    /*
    <ul>
        <li class="current"><a href="#">Daily</a></li>
        <li><a href="#">Monthly</a></li>
    </ul>
    */

  }
}

export default DatePicker


================================================
FILE: assets/src/js/components/Gearwheel.js
================================================
'use strict';

import { h, Component } from 'preact';

class Gearwheel extends Component {
  render(props, state) {
    // don't show if visible prop is false
    if(!props.visible) {
      return '';
    }

    return (
        <li class="settings">
            <a href="javascript:void(0);" onClick={props.onClick}><svg role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M444.788 291.1l42.616 24.599c4.867 2.809 7.126 8.618 5.459 13.985-11.07 35.642-29.97 67.842-54.689 94.586a12.016 12.016 0 0 1-14.832 2.254l-42.584-24.595a191.577 191.577 0 0 1-60.759 35.13v49.182a12.01 12.01 0 0 1-9.377 11.718c-34.956 7.85-72.499 8.256-109.219.007-5.49-1.233-9.403-6.096-9.403-11.723v-49.184a191.555 191.555 0 0 1-60.759-35.13l-42.584 24.595a12.016 12.016 0 0 1-14.832-2.254c-24.718-26.744-43.619-58.944-54.689-94.586-1.667-5.366.592-11.175 5.459-13.985L67.212 291.1a193.48 193.48 0 0 1 0-70.199l-42.616-24.599c-4.867-2.809-7.126-8.618-5.459-13.985 11.07-35.642 29.97-67.842 54.689-94.586a12.016 12.016 0 0 1 14.832-2.254l42.584 24.595a191.577 191.577 0 0 1 60.759-35.13V25.759a12.01 12.01 0 0 1 9.377-11.718c34.956-7.85 72.499-8.256 109.219-.007 5.49 1.233 9.403 6.096 9.403 11.723v49.184a191.555 191.555 0 0 1 60.759 35.13l42.584-24.595a12.016 12.016 0 0 1 14.832 2.254c24.718 26.744 43.619 58.944 54.689 94.586 1.667 5.366-.592 11.175-5.459 13.985L444.788 220.9a193.485 193.485 0 0 1 0 70.2zM336 256c0-44.112-35.888-80-80-80s-80 35.888-80 80 35.888 80 80 80 80-35.888 80-80z"></path></svg></a>
        </li>
    )
  }
}

export default Gearwheel


================================================
FILE: assets/src/js/components/LoginForm.js
================================================
'use strict';

import { h, render, Component } from 'preact';
import Client from '../lib/client.js';
import Notification from '../components/Notification.js';
import { bind } from 'decko';

class LoginForm extends Component {

  constructor(props) {
    super(props)
    this.state = {
      email: '',
      password: '',
      message: ''
    }
  }

  @bind
  handleSubmit(e) {
    e.preventDefault();
    this.setState({ message: '' });

    Client.request('session', {
      method: "POST",
      data: {
        email: this.state.email,
        password: this.state.password,
      }
    }).then((r) => {
        this.props.onSuccess()
    }).catch((e) => {
      this.setState({
        message: e.code === 'invalid_credentials' ? "Invalid username or password" : e.message,
        password: ''
      });
    });
  }

  @bind
  updatePassword(e) {
    this.setState({ password: e.target.value });
  }

  @bind
  updateEmail(e) {
    this.setState({ email: e.target.value });
  }

  @bind
  clearMessage() {
    this.setState({ message: '' });
  }

  render(props, state) {
    return (
      <form method="POST" onSubmit={this.handleSubmit}>
        <div class="">
          <label><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><path d="M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm128 421.6c-35.9 26.5-80.1 42.4-128 42.4s-92.1-15.9-128-42.4V416c0-35.3 28.7-64 64-64 11.1 0 27.5 11.4 64 11.4 36.6 0 52.8-11.4 64-11.4 35.3 0 64 28.7 64 64v13.6zm30.6-27.5c-6.8-46.4-46.3-82.1-94.6-82.1-20.5 0-30.4 11.4-64 11.4S204.6 320 184 320c-48.3 0-87.8 35.7-94.6 82.1C53.9 363.6 32 312.4 32 256c0-119.1 96.9-216 216-216s216 96.9 216 216c0 56.4-21.9 107.6-57.4 146.1zM248 120c-48.6 0-88 39.4-88 88s39.4 88 88 88 88-39.4 88-88-39.4-88-88-88zm0 144c-30.9 0-56-25.1-56-56s25.1-56 56-56 56 25.1 56 56-25.1 56-56 56z"/></svg></label>
          <input type="email" name="email" placeholder="Email address" required="" value={state.email} onInput={this.updateEmail}  />
         </div>
        
        <div class="">
          <label><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M224 420c-11 0-20-9-20-20v-64c0-11 9-20 20-20s20 9 20 20v64c0 11-9 20-20 20zm224-148v192c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V272c0-26.5 21.5-48 48-48h16v-64C64 71.6 136-.3 224.5 0 312.9.3 384 73.1 384 161.5V224h16c26.5 0 48 21.5 48 48zM96 224h256v-64c0-70.6-57.4-128-128-128S96 89.4 96 160v64zm320 240V272c0-8.8-7.2-16-16-16H48c-8.8 0-16 7.2-16 16v192c0 8.8 7.2 16 16 16h352c8.8 0 16-7.2 16-16z"/></svg></label>
          <input type="password" name="password" placeholder="**********" required="" autocomplete="off" value={state.password} onInput={this.updatePassword} />
        </div>
         
        <div><button type="submit">Sign in</button></div>

        <Notification message={state.message} kind="" onDismiss={this.clearMessage} />
      </form>
    )
  }
}

export default LoginForm


================================================
FILE: assets/src/js/components/LogoutButton.js
================================================
'use strict';

import { h, Component } from 'preact';
import Client from '../lib/client.js';
import { bind } from 'decko';

class LogoutButton extends Component {

  @bind
  handleSubmit(e) {
    e.preventDefault();

    Client.request('session', {
      method: "DELETE",
    }).then((r) => { this.props.onSuccess() })
  }

  render() {
    if(document.cookie.indexOf('auth') < 0) {
      return ''
    }

    return (
      <a href="#" onClick={this.handleSubmit}>Sign out</a>
    )
  }
}

export default LogoutButton


================================================
FILE: assets/src/js/components/Notification.js
================================================
'use strict'

import { h, Component } from 'preact';
import { bind } from 'decko';

class Notification extends Component {
  constructor(props) {
    super(props)

    this.state = {
      message: props.message,
      kind: props.kind || 'error'
    }
    this.timeout = 0
  }

  componentWillReceiveProps(newProps) {
    if(newProps.message === this.state.message) {
      return;
    }

    this.setState({ 
      message: newProps.message, 
      kind: newProps.kind || 'error' 
    })
    
    window.clearTimeout(this.timeout)
    this.timeout = window.setTimeout(this.props.onDismiss, 5000)
  }

  render(props, state) {
    if(state.message === '') {
      return ''
    }

    return (
      <div class={`notification`}>
        <div class={`notification-${state.kind}`}>
          {state.message}
        </div>
      </div>
  )}
}

export default Notification


================================================
FILE: assets/src/js/components/Pikadayer.js
================================================
'use strict';

import Pikaday from 'pikaday';
import { h, Component } from 'preact';

class Pikadayer extends Component {
  componentDidMount() {
    this.pikaday = new Pikaday({ 
      field: this.base,
      onSelect: this.props.onSelect,
      position: 'bottom right',
   })
  }

  componentWillReceiveProps(newProps) {
    // make sure pikaday updates if we set a date using one of our presets
    if(this.pikaday && newProps.value !== this.props.value) {
      this.pikaday.setDate(newProps.value, true)
    }
  }

  componentWillUnmount() {
    this.pikaday.destroy()
  }

  render(props) {
    return <input value={props.value} />
  }
}

export default Pikadayer


================================================
FILE: assets/src/js/components/Realtime.js
================================================
'use strict';

import { h, Component } from 'preact';
import Client from '../lib/client.js';
import { bind } from 'decko';
import * as numbers from '../lib/numbers.js';

class Realtime extends Component {

  constructor(props) {
    super(props)

    this.state = {
      count: 0
    }
  }

  componentDidMount() {
      this.fetchData(this.props.siteId);
      this.interval = window.setInterval(this.handleIntervalEvent, 15000);
  }

  componentWillUnmount() {
      window.clearInterval(this.interval);
  }

  componentWillReceiveProps(newProps, newState) {
    if(!this.paramsChanged(this.props, newProps)) {
      return;
    }

    this.fetchData(newProps.siteId)
  }

  paramsChanged(o, n) {
    return o.siteId != n.siteId;
  }

  @bind
  setDocumentTitle() {
    // update document title
    let visitorText = this.state.count == 1 ? 'visitor' : 'visitors';
    document.title = ( this.state.count > 0 ? `${numbers.formatPretty(this.state.count)} current ${visitorText} — Fathom` : 'Fathom' );
  }

  @bind 
  handleIntervalEvent() {
    this.fetchData(this.props.siteId)
  }

  @bind
  fetchData(siteId) {
    let url = `/sites/${siteId}/stats/site/realtime`
    Client.request(url)
      .then((d) => { 
        this.setState({ count: d })
        this.setDocumentTitle();
      })
  }

  render(props, state) {
    let visitorText = state.count == 1 ? 'visitor' : 'visitors';
    return (
        <span><span class="count">{numbers.formatPretty(state.count)}</span> <span>current {visitorText}</span></span>
    )
  }
}

export default Realtime


================================================
FILE: assets/src/js/components/Sidebar.js
================================================
'use strict';

import { h, Component } from 'preact';
import Client from '../lib/client.js';
import { bind } from 'decko';
import CountWidget from './CountWidget.js';


class Sidebar extends Component {
  constructor(props) {
    super(props)

    this.state = {
      data: {},
      loading: false,
    }
  }

  componentWillReceiveProps(newProps, newState) {
    if(!this.paramsChanged(this.props, newProps)) {
      return;
    }

    this.fetchData(newProps);
  }

  paramsChanged(o, n) {
    return o.siteId != n.siteId || o.dateRange[0] != n.dateRange[0] || o.dateRange[1] != n.dateRange[1];
  }

  @bind
  fetchData(props) {
    this.setState({ loading: true })
    let before = props.dateRange[1]/1000;
    let after = props.dateRange[0]/1000;

    Client.request(`/sites/${props.siteId}/stats/site/agg?before=${before}&after=${after}`)
      .then((data) => { 
        // request finished; check if timestamp range is still the one user wants to see
        if(this.paramsChanged(props, this.props)) {
          return;
        }

        // Make sure we always show at least 1 visitor when there are pageviews
        if ( data.Visitors == 0 && data.Pageviews > 0 ) {
          data.Visitors = 1
        }

        this.setState({ 
          loading: false,
          data: data
        })
      })
  }

  render(props, state) {
    return (
      <div class="box box-totals">
        <CountWidget title="Unique visitors" value={state.data.Visitors} loading={state.loading} />
        <CountWidget title="Pageviews" value={state.data.Pageviews} loading={state.loading} />
        <CountWidget title="Avg time on site" value={state.data.AvgDuration} format="duration" loading={state.loading} />
        <CountWidget title="Bounce rate" value={state.data.BounceRate} format="percentage" loading={state.loading} />
      </div>
    )
  }
}

export default Sidebar


================================================
FILE: assets/src/js/components/SiteSettings.js
================================================
'use strict';

import { h, Component } from 'preact';
import Client from '../lib/client.js';
import { bind } from 'decko';

class SiteSettings extends Component {
    constructor(props) {
        super(props)

        this.state = {
            copied: false,
            updated: false,
        }
    }

    componentDidMount() {
        document.addEventListener('keydown', this.handleKeydownEvent);
    }
    componentWillUnmount() {
        document.removeEventListener('keydown', this.handleKeydownEvent)
    }

    @bind
    revertTemporaryState() {
        this.setState({
            copied: false, 
            updated: false
        })
    }

    @bind
    copyToClipboard(evt) {
        this.textarea.select()
        document.execCommand('copy')
        this.setState({ copied: true })
        window.setTimeout(this.revertTemporaryState, 2400)
    }

    @bind 
    deleteSite(evt) {
        if(!confirm("Are you sure you want to delete this site? This action is irreversible - you will lose all the site's data.")) {
            return;
        }

        let site = this.props.site;
        Client.request(`/sites/${site.id}`, {
            method: "DELETE",
          }).then((d) => {
              this.props.onDelete(site)
          })
    }

    @bind 
    onSubmit(evt) {
        evt.preventDefault();
        let site = this.props.site;
        let url = site.unsaved ? `/sites` : `/sites/${site.id}`

        Client.request(url, {
            method: "POST",
            data: {
                name: site.name,
            },
          }).then((site) => {
            this.setState({ updated: true})
            window.setTimeout(this.revertTemporaryState, 2400)

            site.unsaved = false
            this.props.onUpdate(site)
          })
    }

    @bind 
    handleTextareaClickEvent(evt) {
        evt.target.select()
    }

    @bind 
    handleClickEvent(evt) {
        // don't close if click was inside the modal
        if ( evt.target.matches('.modal *, .modal')) {
            return;
        }

        this.props.onClose()
    }

    @bind 
    handleKeydownEvent(evt) {
        // close modal when pressing ESC 
        if(evt.which == 27) {
            this.props.onClose()
        }
    }

    @bind
    setTextarea(el) {
       this.textarea = el
    }

    @bind 
    updateSiteName(evt) {
        this.props.site.name = evt.target.value;
    }

    render(props, state) {
        return (
        <div class="modal-wrap" style={"display: " + ( props.visible ? '' : 'none')} onClick={this.handleClickEvent}>
            <div class="modal">
                <p>{props.site.unsaved ? 'Add a new site to track with Fathom' : 'Update your site name or get your tracking code'}</p>
                <form onSubmit={this.onSubmit}>
                    <fieldset>
                        <label for="site-name">Site name</label>
                        <input type="text" name="site-name" id="site-name" placeholder="" onChange={this.updateSiteName} value={props.site.name} />
                    </fieldset>

                    <fieldset style={props.site.unsaved ? 'display: none;' : ''}>
                        <label>Add this code to your website    <small class="right">(site ID = {props.site.trackingId})</small></label>
                        <textarea ref={this.setTextarea} onFocus={this.handleTextareaClickEvent} readonly="readonly">{`<!-- Fathom - simple website analytics - https://github.com/usefathom/fathom -->
<script>
(function(f, a, t, h, o, m){
	a[h]=a[h]||function(){
		(a[h].q=a[h].q||[]).push(arguments)
	};
	o=f.createElement('script'),
	m=f.getElementsByTagName('script')[0];
	o.async=1; o.src=t; o.id='fathom-script';
	m.parentNode.insertBefore(o,m)
})(document, window, '//${location.host}/tracker.js', 'fathom');
fathom('set', 'siteId', '${props.site.trackingId}');
fathom('trackPageview');
</script>
<!-- / Fathom -->`}
                    </textarea>
                    <small><a href="javascript:void(0);" onClick={this.copyToClipboard}>{state.copied ? "Copied!" : "Copy code"}</a></small>
                </fieldset>

                <fieldset>
                    <div class="half">
                        <div class="submit"><button type="submit">{props.site.unsaved ? 'Create site' : 'Update site name'}</button> &nbsp; {state.updated ? 'Saved!' : ''}</div>
                        {props.site.unsaved ? '' : (<div class="delete"><a href="javascript:void(0);" onClick={this.deleteSite}>Delete site</a></div>)}
                    </div>
                </fieldset>
            </form>
        </div>
    </div>)
    }
}

export default SiteSettings


================================================
FILE: assets/src/js/components/SiteSwitcher.js
================================================
'use strict';

import { h, Component } from 'preact';
import { bind } from 'decko';
import { hashParams } from "../lib/util";



function arrayToQueryString(array_in){
    var out = new Array();

    for(var key in array_in){
        out.push(key + '=' + encodeURIComponent(array_in[key]));
    }

    return out.join('&');
}
class SiteSwitcher extends Component {
  constructor() {
    super();
    this.state = {
      isExpanded: false
    };
  }

  @bind 
  selectSite(evt) {
    let itemId = evt.target.getAttribute("data-id")  
    this.props.sites.some((s) => {
        if (s.id != itemId) {
            return false;
        }
        let params = hashParams()
        params["site"] = s.id
        window.history.replaceState(this.state, null, `#!${arrayToQueryString(params)}`)
        this.props.onChange(s)
        return true;
    })   
  }

  @bind 
  addSite() {
      this.props.onAdd({ id: 1, name: "New site", unsaved: true })
  }

  @bind
  expand() {
    this.setState({
      isExpanded: true
    });
  }

  @bind
  collapse() {
    this.setState({
      isExpanded: false
    });
  }

  @bind
  toggleExpanded() {
    this.setState({
      isExpanded: !this.state.isExpanded
    });
  }

  render(props, state) {
    // show nothing if there is only 1 site and no option to add additional sites
    if(!props.showAdd && props.sites.length == 1) {
        return '';
    }  

    // otherwise, render list of sites + add button
    return (
        <li class={`sites ${state.isExpanded ? 'expanded' : ''}`} onClick={this.toggleExpanded} onMouseEnter={this.expand} onMouseLeave={this.collapse}>
            <a href="javascript:void(0)">{props.selectedSite.name}</a>
            <ul>
                {props.sites.map((s) => (<li class="site-switch"><a href="javascript:void(0);" data-id={s.id} onClick={this.selectSite}>{s.name}</a></li>)) }
                {props.showAdd ? (<li class="add-new"><a href="javascript:void(0);" onClick={this.addSite}>+ Add another site</a></li>) : ''}
            </ul>
        </li>
    )
  }
}

export default SiteSwitcher


================================================
FILE: assets/src/js/components/Table.js
================================================
'use strict';

import { h, Component } from 'preact';
import * as numbers from '../lib/numbers.js';
import Client from '../lib/client.js';
import { bind } from 'decko';
import classNames from 'classnames';
import { runInNewContext } from 'vm';

const dayInSeconds = 60 * 60 * 24;

class Table extends Component {

  constructor(props) {
    super(props)

    this.state = {
      records: [],
      offset: 0,
      limit: 15,
      loading: true,
      total: 0,
    }
  }

  componentWillReceiveProps(newProps, newState) {
    if(!this.paramsChanged(this.props, newProps)) {
      return;
    }

    this.fetchData(newProps)
  }

  paramsChanged(o, n) {
    return o.siteId !== n.siteId || o.dateRange[0] != n.dateRange[0] || o.dateRange[1] != n.dateRange[1];
  }
  
  @bind
  fetchData(props) {
    this.setState({ loading: true });
    let before = props.dateRange[1]/1000;
    let after = props.dateRange[0]/1000;

    Client.request(`/sites/${props.siteId}/stats/${props.endpoint}/agg?before=${before}&after=${after}&offset=${this.state.offset}&limit=${this.state.limit}`)
      .then((d) => {
         // request finished; check if timestamp range is still the one user wants to see
        if( this.paramsChanged(props, this.props) ) {
          return;
        }

        this.setState({
          loading: false,
          records: d,
        });
      });

     // fetch totals too
     Client.request(`/sites/${props.siteId}/stats/${props.endpoint}/agg/pageviews?before=${before}&after=${after}`)
      .then((d) => {
        this.setState({
          total: d
        });
      });
  }

  @bind 
  paginateNext() {
    this.setState({ offset: this.state.offset + this.state.limit })
    this.fetchData(this.props)
  }

  @bind 
  paginatePrev() {
    if(this.state.offset == 0) {
      return;
    }

    this.setState({ offset: Math.max(0, this.state.offset - this.state.limit) })
    this.fetchData(this.props)
  }

  render(props, state) {
    const tableRows = state.records !== null && state.records.length > 0 ? state.records.map((p, i) => {
      let href = (p.Hostname + p.Pathname) || p.URL;
      let widthClass = "";
      if(state.total > 0) {
        widthClass = "w" + ("" + Math.min(98, Math.round(p.Pageviews / state.total * 100 * 2.5))).padStart(2, '0');
      }

      let label = p.Pathname
      if( props.showHostname ) {
        if( p.Group) {
          label = p.Group
        } else {
          label = p.Hostname.replace('www.', '').replace('https://', '').replace('http://', '') + (p.Pathname.length > 1 ? p.Pathname : '')
        }
      }

      return(
      <div class={classNames("table-row", widthClass)}>
        <div class="cell main-col"><a href={href}>{label}</a></div>
        <div class="cell">{numbers.formatPretty(p.Pageviews)}</div>
        <div class="cell">{numbers.formatPretty(p.Visitors)||"-"}</div>
      </div>
    )}) : <div class="table-row"><div class="cell main-col">Nothing here, yet.</div></div>;

  // pagination row: only show when total # of results doesn't fit in one table page  
  const pagination = tableRows.length == state.limit || state.offset >= state.limit ? (
    <div class="row pag">
      <a href="javascript:void(0)" onClick={this.paginatePrev} class="back">‹</a>
      <a href="javascript:void(0)" onClick={this.paginateNext} class="next right">›</a>
    </div>) : '';

    return (
      <div class={classNames({ loading: state.loading })}>
        <div class="table-row header">
          {props.headers.map((header, i) => {
            return <div class={classNames("cell", { "main-col": i === 0 })}>{header}</div>
          })}
        </div>
        <div>
          {tableRows}
          {pagination}
        </div>
      </div>
    )
  }
}

export default Table


================================================
FILE: assets/src/js/lib/client.js
================================================
'use strict';

var Client = {};
Client.request = function(url, args) {
  args = args || {};
  args.credentials = 'same-origin'
  args.headers = args.headers || {};
  args.headers['Accept'] = 'application/json';

  if( args.method && args.method === 'POST') {
    args.headers['Content-Type'] = 'application/json';

    if(args.data) {
      if( typeof(args.data) !== "string") {
        args.data = JSON.stringify(args.data)
      }
      args.body = args.data
      delete args.data
    }
  }

  // trim leading slash from URL
  url = (url[0] === '/') ? url.substring(1) : url;

  return window.fetch(`api/${url}`, args)
    .then(handleRequestErrors)
    .then(parseJSON)
    .then(parseData)
}

function handleRequestErrors(r) {
  // if response is not JSON (eg timeout), throw a generic error
  if (! r.ok && r.headers.get("Content-Type") !== "application/json") {
    throw { code: "request_error", message: "An error occurred" }
  }

  return r
}

function parseJSON(r) {
  return r.json()
}

function parseData(d) {

  // if JSON response contains an Error property, use that as error code
  // Message is generic here, so that individual components can set their own specific messages based on the error code
  if(d.Error) {
    throw { code: d.Error, message: "An error occurred" }
  }

  return d.Data
}

export default Client


================================================
FILE: assets/src/js/lib/numbers.js
================================================
'use strict';

const M = 1000000
const K = 1000
const rx = new RegExp('\\.0$');
const commaRx = new RegExp('(\\d+)(\\d{3})');

function formatPretty(num) {
  let decimals = 0;

  if (num >= M) {
    num /= M
    decimals = 3 - ((Math.round(num) + "").length) || 0;
    return (num.toFixed(decimals > -1 ? decimals : 0).replace(rx, '') + 'M').replace('.00', '');
  }

  if (num >= (K * 10)) {
    num /= K
    decimals = 3 - ((Math.round(num) + "").length) || 0;
    return num.toFixed(decimals).replace(rx, '') + 'K';
  }

  return formatWithComma(num);
}

function formatWithComma(nStr) {
	nStr += '';

  if(nStr.length < 4 ) {
    return nStr;
  }

  var	x = nStr.split('.');
	var x1 = x[0];
	var x2 = x.length > 1 ? '.' + x[1] : '';
	while (commaRx.test(x1)) {
		x1 = x1.replace(commaRx, '$1' + ',' + '$2');
	}
	return x1 + x2;
}

function formatDuration(seconds) {
  seconds = Math.round(seconds);
  var date = new Date(null);
  date.setSeconds(seconds); // specify value for SECONDS here
  return date.toISOString().substr(14, 5);
}

function formatPercentage(p) {
   return Math.round(p*100) + "%";
}

export { 
  formatPretty,
  formatWithComma, 
  formatDuration, 
  formatPercentage 
}


================================================
FILE: assets/src/js/lib/util.js
================================================
'use strict';

// convert object to query string
function stringifyObject(json) {
    var keys = Object.keys(json);

    return '?' +
        keys.map(function (k) {
            return encodeURIComponent(k) + '=' +
                encodeURIComponent(json[k]);
        }).join('&');
}

function randomString(n) {
    var s = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    return Array(n).join().split(',').map(() => s.charAt(Math.floor(Math.random() * s.length))).join('');
}

function hashParams() {
    var params = {},
        match,
        matches = window.location.hash.substring(2).split("&");

    for (var i = 0; i < matches.length; i++) {
        match = matches[i].split('=')
        params[match[0]] = decodeURIComponent(match[1]);
    }

    return params;
}

export {
    randomString,
    stringifyObject,
    hashParams
}


================================================
FILE: assets/src/js/pages/dashboard.js
================================================
'use strict'

import { h, Component } from 'preact';
import LogoutButton from '../components/LogoutButton.js';
import Realtime from '../components/Realtime.js';
import DatePicker from '../components/DatePicker.js';
import Sidebar from '../components/Sidebar.js';
import SiteSwitcher from '../components/SiteSwitcher.js';
import SiteSettings from '../components/SiteSettings.js';
import Gearwheel from '../components/Gearwheel.js';
import Table from '../components/Table.js';
import Chart from '../components/Chart.js';
import { bind } from 'decko';
import Client from '../lib/client.js';
import classNames from 'classnames';
import {hashParams} from "../lib/util";


let defaultSite = {}
class Dashboard extends Component {
  constructor(props) {
    super(props)
    let params = hashParams()
    defaultSite = {
      id: params.site || window.localStorage.getItem('site_id') || 1,
      name: "",
      unsaved: true,
    };
    this.state = {
      dateRange: [],
      groupBy: 'day',
      isPublic: document.cookie.indexOf('auth') < 0,
      site: defaultSite,
      sites: [],
      settingsOpen: false,
      addingNewSite: false,
    }
  }

  componentDidMount() {
    this.fetchSites()
  }

  @bind
  fetchSites() {
    Client.request(`sites`)
    .then((sites) => {
      // open site settings when there are no sites yet
      if(sites.length == 0) {
        this.showSiteSettings({ id: 1, name: "yoursite.com", unsaved: true })
        return;
      }

      // if there are sites, use remembered site as selected site
      let site = sites[0];
      let s = sites.find(s => s.id == defaultSite.id);
      site = s ? s : site;

      this.setState({
        sites: sites,
        site: site,
      })
    }).catch((e) => {
      if(e.code === 'unauthorized') {
        this.props.onLogout()
      }
    })
  }

  @bind
  changeDateRange(s) {
    this.setState({
      dateRange: [ s.startDate, s.endDate ],
      period: s.period,
      groupBy: s.groupBy,
    })
  }

  @bind
  showSiteSettings(site) {
    site = site && site.unsaved ? site : this.state.site;
    this.setState({
      settingsOpen: true,
      site: site,
      previousSite: this.state.site,
    })
  }

  @bind
  closeSiteSettings() {
    this.setState({
      settingsOpen: false,

      // switch back to previous site if we were showing site settings to add a new site
      site: this.state.site.unsaved && this.state.previousSite ? this.state.previousSite : this.state.site,
    })
  }

  @bind
  changeSelectedSite(site) {
    let newState = {
      site: site,
    }

    if(!this.state.site.unsaved) {
      newState.previousSite = this.state.site
    }

    this.setState(newState)
    window.localStorage.setItem('site_id', site.id)
  }

  @bind
  updateSite(site) {
    let updated = false;
    let newSites = this.state.sites.map((s) => {
      if(s.id != site.id) {
        return s;
      }

      updated = true;

      // replace site in sites array with parameter
      return site;
    })

    if(!updated) {
      newSites.push(site);
    }

    this.setState({sites: newSites, site: site})
  }

  @bind
  deleteSite(site) {
    let newSites = this.state.sites.filter((s) => (s.id != site.id))
    let newSelectedSite = newSites.length > 0 ? newSites[0] : defaultSite;
    this.setState({
      sites: newSites,
      site: newSelectedSite
    })
  }

  render(props, state) {
    // only show logout link if this dashboard is not public
    let logoutMenuItem = state.isPublic ? '' : (
      <li class="signout"><span class="spacer">&middot;</span><LogoutButton onSuccess={props.onLogout} /></li>
    );

    return (
  <div class="app-page ">
     <div class={`rapper animated fadeInUp delayed_02s ${state.period} ` + classNames({ ltday: state.dateRange[1] - state.dateRange[0] < 86400 })}>

      <header class="section">
        <nav class="main-nav">
            <ul>
              <li class="logo"><a href="/">{state.site.name || "Fathom"}</a></li>
              <SiteSwitcher sites={state.sites} selectedSite={state.site} onChange={this.changeSelectedSite} onAdd={this.showSiteSettings} showAdd={!state.isPublic}/>
              <Gearwheel onClick={this.showSiteSettings} visible={!state.isPublic} />
              <li class="visitors"><Realtime siteId={state.site.id} /></li>
          </ul>
        </nav>
      </header>

      <DatePicker onChange={this.changeDateRange} />

      <section class="section">
        <div class="boxes">
          <Sidebar siteId={state.site.id} dateRange={state.dateRange} />

          <div class="box box-graph">
            <Chart siteId={state.site.id} dateRange={state.dateRange} tickStep={state.groupBy} />
          </div>
          <div class="box box-pages">
            <Table endpoint="pages" headers={["Top pages", "Views", "Uniques"]} siteId={state.site.id} dateRange={state.dateRange} />
          </div>
          <div class="box box-referrers">
            <Table endpoint="referrers" headers={["Top referrers", "Views", "Uniques"]} siteId={state.site.id} dateRange={state.dateRange} showHostname="true" />
          </div>
        </div>
        <div class="notice">
          <div class="notice-text"><strong>Want more features and less maintenance?</strong> Check out the current version of Fathom Analytics and <a href="https://usefathom.com/ref/GITHUB">start your free trial today</a>.</div>
        </div>
        <footer class="section">
          <div class="half">
          <nav>
            <ul>
              <li><a href="https://usefathom.com/">Fathom</a></li>
              <li><a href="https://usefathom.com/terms/">Terms of use</a></li>
              <li><a href="https://usefathom.com/privacy/">Privacy policy</a></li>
              <li><a href="https://usefathom.com/data/">Our data policy</a></li>
              <li><LogoutButton onSuccess={props.onLogout} /></li>
            </ul>
          </nav>
          <div class="hide-on-mobile">Use <strong>the arrow keys</strong> to cycle through date ranges.</div>
          </div>
        </footer>
      </section>
    </div>
    <SiteSettings visible={state.settingsOpen} onClose={this.closeSiteSettings} onUpdate={this.updateSite} onDelete={this.deleteSite} site={state.site} />
  </div>
  )}
}

export default Dashboard


================================================
FILE: assets/src/js/pages/login.js
================================================
'use strict';

import { h, render, Component } from 'preact';
import LoginForm from '../components/LoginForm.js';

class Login extends Component {
  render(props, state) {
    return (
      <div class="flex-rapper login-page animated fadeInUp delayed_02s">
        <div class="login-rapper">
          <LoginForm onSuccess={props.onLogin} />
          <small><a href="https://usefathom.com">Fathom Analytics</a>{/* &middot; <a href="#lost">Password reset</a> */}</small>
        </div>
      </div>
    )
  }
}

export default Login


================================================
FILE: assets/src/js/script.js
================================================
'use strict';

import { h, render, Component } from 'preact'
import Login from './pages/login.js'
import Dashboard from './pages/dashboard.js'
import { bind } from 'decko';
import Client from './lib/client.js';

class App extends Component {
  constructor(props) {
    super(props)

    this.state = {
      authenticated: document.cookie.indexOf('auth') > -1
    }

    this.fetchAuthStatus()
  }

  @bind
  fetchAuthStatus() {
    Client.request(`session`)
      .then((d) => { 
        this.setState({ authenticated: d })
      })
  }

  @bind
  toggleAuth() {
    this.setState({ 
      authenticated: !this.state.authenticated 
    })
  }

  render(props, state) {
    // logged-in
    if( state.authenticated ) {
      return <Dashboard onLogout={this.toggleAuth} />
    }

    // logged-out
    return <Login onLogin={this.toggleAuth} />
  }
}

render(<App />, document.getElementById('root'));


================================================
FILE: assets/src/js/tracker.js
================================================
(function() { 
  'use strict';

  let queue = window.fathom.q || [];
  let config = {
    'siteId': '',
    'trackerUrl': '',
  };
  const commands = {
    "set": set,
    "trackPageview": trackPageview,
    "setTrackerUrl": setTrackerUrl,
  };

  function set(key, value) {
    config[key] = value;
  }

  function setTrackerUrl(value) {
    return set("trackerUrl", value);
  }

  // convert object to query string
  function stringifyObject(obj) {
    var keys = Object.keys(obj);

    return '?' +
        keys.map(function(k) {
            return encodeURIComponent(k) + '=' + encodeURIComponent(obj[k]);
        }).join('&');
  }

  function getCookie(name) {
    var cookies = document.cookie ? document.cookie.split('; ') : [];
    
    for (var i = 0; i < cookies.length; i++) {
      var parts = cookies[i].split('=');
      if (decodeURIComponent(parts[0]) !== name) {
        continue;
      }

      var cookie = parts.slice(1).join('=');
      return decodeURIComponent(cookie);
    }

    return '';
  }

  function setCookie(name, data, args) {
    name = encodeURIComponent(name);
    data = encodeURIComponent(String(data));

    var str = name + '=' + data;

    if(args.path) {
      str += ';path=' + args.path;
    }
    if (args.expires) {
      str += ';expires='+args.expires.toUTCString();
    }

    document.cookie = str + ';SameSite=None;Secure';
  }

  function newVisitorData() {
    return {
      isNewVisitor: true, 
      isNewSession: true,
      pagesViewed: [],
      previousPageviewId: '',
      lastSeen: +new Date(),
    }
  }

  function getData() {
    let thirtyMinsAgo = new Date();
    thirtyMinsAgo.setMinutes(thirtyMinsAgo.getMinutes() - 30);

    let data = getCookie('_fathom');
    if(! data) {
      return newVisitorData();
    }

    try{
      data = JSON.parse(data);
    } catch(e) {
      console.error(e);
      return newVisitorData();
    }

    if(data.lastSeen < (+thirtyMinsAgo)) {
      data.isNewSession = true;
    }

    return data;  
  }

  function findTrackerUrl() {
    const el = document.getElementById('fathom-script')
    return el ? el.src.replace('tracker.js', 'collect') : '';
  }

  function trackPageview(vars) { 
    vars = vars || {};

    // Respect "Do Not Track" requests
    if('doNotTrack' in navigator && navigator.doNotTrack === "1") {
      return;
    }

    // ignore prerendered pages
    if( 'visibilityState' in document && document.visibilityState === 'prerender' ) {
      return;
    }

    // if <body> did not load yet, try again at dom ready event
    if( document.body === null ) {
      document.addEventListener("DOMContentLoaded", () => {
        trackPageview(vars);
      })
      return;
    }

    //  parse request, use canonical if there is one
    let req = window.location;

    // do not track if not served over HTTP or HTTPS (eg from local filesystem) and we're not in an Electron app
    if(req.host === '' && navigator.userAgent.indexOf("Electron") < 0) {
      return;
    }

    // find canonical URL
    let canonical = document.querySelector('link[rel="canonical"][href]');
    if(canonical) {
      let a = document.createElement('a');
      a.href = canonical.href;

      // use parsed canonical as location object
      req = a;
    }
    
    let path = vars.path || ( req.pathname + req.search );
    if(!path) {
      path = '/';
    }

    // determine hostname
    let hostname = vars.hostname || ( req.protocol + "//" + req.hostname );

    // only set referrer if not internal
    let referrer = vars.referrer || '';
    if(document.referrer.indexOf(hostname) < 0) {
      referrer = document.referrer;
    }

    let data = getData();
    const d = {
      pid: data.previousPageviewId || '',
      p: path,
      h: hostname,
      r: referrer,
      u: data.pagesViewed.indexOf(path) == -1 ? 1 : 0,
      nv: data.isNewVisitor ? 1 : 0, 
      ns: data.isNewSession ? 1 : 0,
      sid: config.siteId,
    };

    let url = config.trackerUrl || findTrackerUrl()
    let img = document.createElement('img');
    img.setAttribute('alt', '');
    img.setAttribute('aria-hidden', 'true');
    img.setAttribute('style', 'position:absolute');
    img.src = url + stringifyObject(d);
    img.addEventListener('load', function() {
      let midnight = new Date();
      midnight.setHours(24); midnight.setMinutes(0); midnight.setSeconds(0);

      // update data in cookie
      if( data.pagesViewed.indexOf(path) == -1 ) {
        data.pagesViewed.push(path);
      }
      data.previousPageviewId = d.id;
      data.isNewVisitor = false;
      data.isNewSession = false;
      data.lastSeen = +new Date();
      setCookie('_fathom', JSON.stringify(data), { expires: midnight, path: '/' });

      // remove tracking img from DOM
      document.body.removeChild(img)
    });
    
    // in case img.onload never fires, remove img after 1s & reset src attribute to cancel request
    window.setTimeout(() => { 
      if(!img.parentNode) {
        return;
      }

      img.src = ''; 
      document.body.removeChild(img)
    }, 1000);

    // add to DOM to fire request
    document.body.appendChild(img);  
  }

  // override global fathom object
  window.fathom = function() {
    var args = [].slice.call(arguments);
    var c = args.shift();
    commands[c].apply(this, args);
  };

  // process existing queue
  queue.forEach((i) => fathom.apply(this, i));
})()


================================================
FILE: docker-compose.yml
================================================
version: "3"
services:
  fathom:
    image: usefathom/fathom:latest
    ports:
      - "8080:8080"
    environment:
      - "FATHOM_SERVER_ADDR=:8080"
      - "FATHOM_GZIP=true"
      - "FATHOM_DEBUG=false"
      - "FATHOM_DATABASE_DRIVER=mysql"
      - "FATHOM_DATABASE_NAME=fathom"
      - "FATHOM_DATABASE_USER=fathom"
      - "FATHOM_DATABASE_PASSWORD=password01"
      - "FATHOM_DATABASE_HOST=mysql:3306"
      - "FATHOM_SECRET=TWEn6GXQDx45PZfmJWvyGpXf5M8b94bszgw8JcJWEd6WxgrnUkLatS34GwjPTvZb"
    links:
      - "mysql:mysql"
    depends_on:
      - mysql
    restart: always
  mysql:
    image: "mysql:5"
    volumes:
      - ./mysql-data:/var/lib/mysql
    ports:
      - "127.0.0.1:3306:3306"
    environment:
      - "MYSQL_ALLOW_EMPTY_PASSWORD=false"
      - "MYSQL_DATABASE=fathom"
      - "MYSQL_PASSWORD=password01"
      - "MYSQL_ROOT_PASSWORD=password01"
      - "MYSQL_USER=fathom"
    restart: always


================================================
FILE: docs/Configuration.md
================================================
# Configuring Fathom

All configuration in Fathom is optional. If you supply no configuration values then Fathom will default to using a SQLite database in the current working directory.

If you're already running MySQL or PostgreSQL on the server you're installing Fathom on, you'll most likely want to use one of those as your database driver.

To do so, either create a `.env` file in the working directory of your Fathom application or point Fathom to your configuration file by specifying the `--config` flag when starting Fathom.

`
fathom --config=/home/john/fathom.env server
`

The default configuration looks like this:

```
FATHOM_GZIP=true
FATHOM_DEBUG=true
FATHOM_DATABASE_DRIVER="sqlite3"
FATHOM_DATABASE_NAME="./fathom.db"
FATHOM_DATABASE_USER=""
FATHOM_DATABASE_PASSWORD=""
FATHOM_DATABASE_HOST=""
FATHOM_DATABASE_SSLMODE=""
FATHOM_SECRET="random-secret-string"
```

### Accepted values & defaults

| Name | Default | Description
| :---- | :---| :---
| FATHOM_DEBUG | `false` | If `true` will write more log messages.
| FATHOM_SERVER_ADDR | `:8080` | The server address to listen on
| FATHOM_GZIP | `false` | if `true` will HTTP content gzipped
| FATHOM_DATABASE_DRIVER | `sqlite3` | The database driver to use: `mysql`, `postgres` or `sqlite3`
| FATHOM_DATABASE_NAME |  | The name of the database to connect to (or path to database file if using sqlite3)
| FATHOM_DATABASE_USER |  | Database connection user
| FATHOM_DATABASE_PASSWORD | | Database connection password
| FATHOM_DATABASE_HOST |  | Database connection host
| FATHOM_DATABASE_SSLMODE | | For a list of valid values, look [here for Postgres](https://www.postgresql.org/docs/9.1/static/libpq-ssl.html#LIBPQ-SSL-PROTECTION) and [here for MySQL](https://github.com/Go-SQL-Driver/MySQL/#tls)
| FATHOM_DATABASE_URL | | Can be used to specify the connection string for your database, as an alternative to the previous 5 settings. 
| FATHOM_SECRET |  | Random string, used for signing session cookies

### Common issues

##### Fathom panics when trying to connect to Postgres: `pq: SSL is not enabled on the server`

This usually means that you're running Postgres without SSL enabled. Set the `FATHOM_DATABASE_SSLMODE` config option to remedy this.

```
FATHOM_DATABASE_SSLMODE=disable
```

##### Using `FATHOM_DATABASE_URL`

When using `FATHOM_DATABASE_URL` to manually specify your database connection string, there are a few important things to consider.

- When using MySQL, include `?parseTime=true&loc=Local` in your DSN.
- When using SQLite, include `?_loc=auto` in your DSN.

Examples of valid values:

```
FATHOM_DATABASE_DRIVER=mysql
FATHOM_DATABASE_URL=root:@tcp/fathom1?loc=Local&parseTime=true
```


================================================
FILE: docs/FAQ.md
================================================
# Frequently Asked Questions

### How do I install Fathom on my server?

Have a look at the [installation instructions](Installation%20instructions.md).

---

### How do I upgrade Fathom to the latest version?

By overwriting the fathom binary with the new version. Make sure to restart any running processes for the changes to take effect. More detailed instructions can be found here: [upgrading Fathom](Updating%20to%20the%20latest%20version.md).

---

### What databases can I use with Fathom?

You can use Fathom with either Postgres, MySQL or SQLite. 


---

### How to configure Fathom?

Create a file named `.env` in the working directory of your Fathom process. You can [find a list of accepted configuration values here](Configuration.md).

---

### How to start tracking pageviews?

Add the tracking snippet to all pages on your site that you want to keep track of. Get your tracking snippet by clicking the gearwheel icon in your Fathom dashboard.

---

### What data does Fathom track?

Fathom tracks no personally identifiable information on your visitors. 

When Fathom tracks a pageview, your visitor is assigned a random string which is used to determine whether it's a unique pageview. If your visitor visits another page on your site, the previous pageview is processed & deleted within 1 minute. If the visitor leaves your site, the pageview is processed & deleted when the session ends (in 30 minutes).

If "Do Not Track" is enabled in the browser settings, Fathom respects that.

---

### Fathom is not tracking my pageviews

If you have the tracking snippet in place and Fathom is still not tracking you, most likely you have `navigator.doNotTrack` enabled. Fathom is respecting your browser's "Do Not Track" setting right now.


================================================
FILE: docs/Installation instructions.md
================================================
# Installation instructions for Fathom

To install Fathom on your server: 

1. [Download the latest Fathom release](https://github.com/usefathom/fathom/releases) suitable for your platform.
2. Extract the archive to `/usr/local/bin`

```sh
tar -C /usr/local/bin -xzf fathom_$VERSION_$OS_$ARCH.tar.gz
chmod +x /usr/local/bin/fathom
```

Confirm that Fathom is installed properly by running `fathom --version`

```sh
$ fathom --version
Fathom version 1.0.0
```

## Configuring Fathom

> This step is optional. By default, Fathom will use a SQLite database file in the current working directory.

To run the Fathom web server we will need to [configure Fathom](Configuration.md) so that it can connect with your database of choice. 

Let's create a new directory where we can store our configuration file & SQLite database.

```
mkdir ~/my-fathom-site
cd ~/my-fathom-site
```

Then, create a file named `.env` with the following contents.

```
FATHOM_SERVER_ADDR=9000
FATHOM_GZIP=true
FATHOM_DEBUG=true
FATHOM_DATABASE_DRIVER="sqlite3"
FATHOM_DATABASE_NAME="fathom.db"
FATHOM_SECRET="random-secret-string"
```

If you now run `fathom server` then Fathom will start serving up a website on port 9000 using a SQLite database file named `fathom.db`. If that port is exposed then you should now see your Fathom instance running by browsing to `http://server-ip-address-here:9000`.

Check out the [configuration file documentation](Configuration.md) for all possible configuration values, eg if you want to use MySQL or Postgres instead.

## Register your admin user

> This step is required.

To register a user in the Fathom instance we just created, run the following command from the directory where your `.env` file is. 

```
fathom user add --email="john@email.com" --password="strong-password"
```

**Note:** if you're running Fathom v1.0.1 or older, the command is `fathom register --email="john@email.com" --password="strong-password"`

## Using NGINX with Fathom

We recommend using NGINX with Fathom, as it simplifies running multiple sites from the same server and handling SSL certificates with LetsEncrypt.

Create a new file in `/etc/nginx/sites-enabled/my-fathom-site` with the following contents. Replace `my-fathom-site.com` with the domain you would like to use for accessing your Fathom installation.

```sh
server {
	server_name my-fathom-site.com;

	location / {
		proxy_set_header X-Real-IP $remote_addr;
		proxy_set_header X-Forwarded-For $remote_addr;
		proxy_set_header Host $host;
		proxy_pass http://127.0.0.1:9000; 
	}
}
```

Test your NGINX configuration and reload NGINX.

```
nginx -t
service nginx reload
```

If you now run `fathom server` again, you should be able to access your Fathom installation by browsing to `http://my-fathom-site.com`.

## Automatically starting Fathom on boot

To ensure the Fathom web server keeps running whenever the system reboots, we should use a process manager. Ubuntu 16.04 and later ship with Systemd.

Create a new file called `/etc/systemd/system/my-fathom-site.service` with the following contents. Replace `$USER` with your actual username.

```
[Unit]
Description=Starts the fathom server
Requires=network.target
After=network.target

[Service]
Type=simple
User=$USER
Restart=always
RestartSec=3
WorkingDirectory=/home/$USER/my-fathom-site
ExecStart=/usr/local/bin/fathom server

[Install]
WantedBy=multi-user.target
```

Reload the Systemd configuration & enable our service so that Fathom is automatically started whenever the system boots.

```
systemctl daemon-reload
systemctl enable my-fathom-site
```

You should now be able to manually start your Fathom web server by issuing the following command.

```
systemctl start my-fathom-site
```

## Tracking snippet

To start tracking pageviews, copy the tracking snippet shown in your Fathom dashboard to all pages of the website you want to track.


### SSL certificate

With [Certbot](https://certbot.eff.org/docs/) for LetsEncrypt installed, adding an SSL certificate to your Fathom installation is as easy as running the following command.

```
certbot --nginx -d my-fathom-site.com
```




================================================
FILE: docs/README.md
================================================
Welcome to the fathom wiki!

How to:

* [Install Fathom with our One-Click DigitalOcean installer](DigitalOcean%20One-Click%20Installation%20Instructions.md)
* [Installing and running Fathom](Installation%20instructions.md)
* [Upgrading Fathom to the latest version](Updating%20to%20the%20latest%20version.md)
* [Configuration](Configuration.md)
* [Frequently asked questions](FAQ.md)

Misc:

* [Using Fathom with Systemd](misc/Systemd.md)
* [Running Fathom with NGINX](misc/NGINX.md)
* [Running Fathom on Heroku](misc/Heroku.md)


================================================
FILE: docs/Updating to the latest version.md
================================================
# Updating Fathom to the latest version

To update your existing Fathom installation to the latest version, first rename your existing Fathom installation so that we can move the new version in its place.

```
mv /usr/local/bin/fathom /usr/local/bin/fathom-old
```

Then, [download the latest release archive suitable for your system architecture from the releases page](https://github.com/usefathom/fathom/releases/latest) and place it in `/usr/local/bin`.

```
tar -C /usr/local/bin -xzf fathom_$VERSION_$OS_$ARCH.tar.gz
chmod +x /usr/local/bin/fathom
``` 

If you now run `fathom --version`, you should see that your system is running the latest version. 

```
$ fathom --version
Fathom version 1.0.0
```


### Restarting your Fathom web server

To start serving up the updated Fathom web application, you will have to restart the Fathom process that is running the web server.

If you've followed the [installation instructions](Installation%20instructions.md) then you are using Systemd to manage the Fathom process. Run `systemctl restart <your-fathom-service>` to restart it.

```
systemctl restart my-fathom-site
```

Alternatively, kill all running Fathom process by issuing the following command.

```
pkill fathom
```


================================================
FILE: docs/misc/Heroku.md
================================================
# Running Fathom on Heroku

### Requirements

* heroku cli (logged in)
* git
* curl
* wget
* tar are required
* ~ openssl is required to generate the secret_key, but you're free to use what you want

### Create the app

First you need to choose a unique app name, as Heroku generates a subdomain for your app.

* create the app via the buildpack

```bash
heroku create UNIQUE_APP_NAME --buildpack https://github.com/ph3nx/heroku-binary-buildpack.git
```

* locally clone the newly created app

```bash
heroku git:clone -a UNIQUE_APP_NAME
cd UNIQUE_APP_NAME
```

* create the folder that will contain fathom

```bash
mkdir -p bin
```

* download latest version of fathom for linux 64bit

```bash
curl -s https://api.github.com/repos/usefathom/fathom/releases/latest \
  | grep browser_download_url \
  | grep linux_amd64.tar.gz \
  | cut -d '"' -f 4 \
  | wget -qi - -O- \
  | tar --directory bin -xz - fathom
```

* create the Procfile for Heroku

```bash
echo "web: bin/fathom server" > Procfile
```

* create a Postgres database (you can change the type of plan if you want - https://elements.heroku.com/addons/heroku-postgresql#pricing)

```bash
heroku addons:create heroku-postgresql:hobby-dev
```

* update the environment variables, generate a secret_key

here you can change the way you generate your secret_key.

```bash
heroku config:set PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/app/bin \
  FATHOM_DATABASE_DRIVER=postgres \
  FATHOM_DATABASE_URL=$(heroku config:get DATABASE_URL) \
  FATHOM_DEBUG=true \
  FATHOM_SECRET=$(openssl rand -base64 32) \
  FATHOM_GZIP=true
```

* add, commit and push all our files

```bash
git add --all
git commit -m "First Commit"
git push heroku master
```

* the created app runs as a free-tier. A free-tier dyno uses the account-based pool
of free dyno hours. If you have other free dynos running, you will need to upgrade your app to a 'hobby' one. - https://www.heroku.com/pricing

```bash
heroku dyno:resize hobby
```

* check that everything is working

```bash
heroku run fathom --version
```

* add the first user

```bash
heroku run fathom user add --email="test@test.com" --password="test_password"
```

* open the browser to login and add your first website

```bash
heroku open
```

* ENJOY :)


================================================
FILE: docs/misc/NGINX.md
================================================
# Using NGINX with Fathom

Let's say you have the Fathom server listening on port 9000 and want to serve it on your domain, `yourfathom.com`.

We can use NGINX to redirect all traffic for a certain domain to our Fathom application by using the `proxy_pass` directive combined with the port Fathom is listening on. 

Create the following file in `/etc/nginx/sites-enabled/yourfathom.com`

```
server {
	server_name yourfathom.com;

	location / {
		proxy_set_header X-Real-IP $remote_addr;
		proxy_set_header X-Forwarded-For $remote_addr;
		proxy_set_header Host $host;
		proxy_pass http://127.0.0.1:9000; 
	}
}
```

If you wish to protect your site using a [Let's Encrypt](https://letsencrypt.org/) HTTPS certificate, you can do so using the [Certbot webroot plugin](https://certbot.eff.org/docs/using.html#webroot). 

```
certbot certonly --webroot --webroot-path /var/www/yourfathom.com -d yourfathom.com
```

Your `/etc/nginx/sites-enabled/yourfathom.com` file should be updated accordingly:

```
server {
	listen 443 ssl http2;
	listen [::]:443 ssl http2;

	server_name yourfathom.com;

	ssl_certificate /path/to/your/fullchain.pem;
	ssl_certificate_key /path/to/your/privkey.pem;

	location /.well-known {
		alias /var/www/yourfathom.com/.well-known;
	}

	location / {
		proxy_set_header X-Real-IP $remote_addr;
		proxy_set_header X-Forwarded-For $remote_addr;
		proxy_set_header Host $host;
		proxy_pass http://127.0.0.1:9000; 
	}
}
```

The `alias` directive should point to the location where your `--webroot-path` is specified when generating the certificate (with `/.well-known` appended).

### Test NGINX configuration
```
sudo nginx -t
```

### Reload NGINX configuration

```
sudo service nginx reload
```


================================================
FILE: docs/misc/Systemd.md
================================================
# Managing the Fathom process with Systemd

To run Fathom as a service (so it keeps on running in the background and is automatically restarted in case of a server reboot) on Ubuntu 16.04 or later, first ensure you have the `fathom` binary installed and in your `$PATH` so that the command exists.

Then, create a new service config file in the `/etc/systemd/system/` directory.

Example file: `/etc/systemd/system/fathom.service`

The file should have the following contents, with `$USER` substituted with your actual username.

```
[Unit]
Description=Starts the fathom server
Requires=network.target
After=network.target

[Service]
Type=simple
User=$USER
Restart=always
RestartSec=6
WorkingDirectory=/etc/fathom # (or where fathom should store its files)
ExecStart=fathom server

[Install]
WantedBy=multi-user.target
```

Save the file and run `sudo systemctl daemon-reload` to load the changes from disk. 

Then, run `sudo systemctl enable fathom` to start the service whenever the system boots.

### Starting or stopping the Fathom service manually
```
sudo systemctl start fathom
sudo systemctl stop fathom
```

### Using a custom configuration file

If you want to [modify the configuration values for your Fathom service](../Configuration.md), then change the line starting with `ExecStart=...` to include the path to your configuration file.

For example, if you have a configuration file `/home/john/fathom.env` then the line should look like this:

```
ExecStart=fathom --config=/home/john/fathom.env server --addr=:9000
```

#### Start Fathom automatically at boot
```
sudo systemctl enable fathom
```

#### Stop Fathom from starting at boot

```
sudo systemctl disable fathom
```


================================================
FILE: go.mod
================================================
module github.com/usefathom/fathom

go 1.19

require (
	github.com/go-sql-driver/mysql v1.6.0
	github.com/gobuffalo/packr/v2 v2.8.3
	github.com/google/uuid v1.3.0
	github.com/gorilla/context v1.1.1
	github.com/gorilla/handlers v1.5.1
	github.com/gorilla/mux v1.8.0
	github.com/gorilla/sessions v1.2.1
	github.com/jmoiron/sqlx v1.3.5
	github.com/joho/godotenv v1.4.0
	github.com/kelseyhightower/envconfig v1.4.0
	github.com/lib/pq v1.10.7
	github.com/mattn/go-sqlite3 v1.14.16
	github.com/mssola/user_agent v0.5.3
	github.com/rubenv/sql-migrate v1.2.0
	github.com/sirupsen/logrus v1.9.0
	github.com/urfave/cli v1.22.10
	golang.org/x/crypto v0.3.0
)

require (
	github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
	github.com/felixge/httpsnoop v1.0.1 // indirect
	github.com/go-gorp/gorp/v3 v3.0.2 // indirect
	github.com/gobuffalo/logger v1.0.6 // indirect
	github.com/gobuffalo/packd v1.0.1 // indirect
	github.com/gorilla/securecookie v1.1.2-0.20180608144417-78f3d318a8bf // indirect
	github.com/karrick/godirwalk v1.16.1 // indirect
	github.com/markbates/errx v1.1.0 // indirect
	github.com/markbates/oncer v1.0.0 // indirect
	github.com/markbates/safe v1.0.1 // indirect
	github.com/russross/blackfriday/v2 v2.0.1 // indirect
	github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
	github.com/ziutek/mymysql v1.5.5-0.20171217234033-ff6cc86d3d93 // indirect
	golang.org/x/net v0.2.0 // indirect
	golang.org/x/sys v0.2.0 // indirect
	golang.org/x/term v0.2.0 // indirect
	golang.org/x/text v0.4.0 // indirect
)


================================================
FILE: go.sum
================================================
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Masterminds/sprig/v3 v3.2.0/go.mod h1:tWhwTbUTndesPNeF0C900vKoq283u6zp4APT9vaF3SI=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gorp/gorp/v3 v3.0.2 h1:ULqJXIekoqMx29FI5ekXXFoH1dT2Vc8UhnRzBg+Emz4=
github.com/go-gorp/gorp/v3 v3.0.2/go.mod h1:BJ3q1ejpV8cVALtcXvXaXyTOlMmJhWDxTmncaR6rwBY=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/gobuffalo/logger v1.0.6 h1:nnZNpxYo0zx+Aj9RfMPBm+x9zAU2OayFh/xrAWi34HU=
github.com/gobuffalo/logger v1.0.6/go.mod h1:J31TBEHR1QLV2683OXTAItYIg8pv2JMHnF/quuAbMjs=
github.com/gobuffalo/packd v1.0.1 h1:U2wXfRr4E9DH8IdsDLlRFwTZTK7hLfq9qT/QHXGVe/0=
github.com/gobuffalo/packd v1.0.1/go.mod h1:PP2POP3p3RXGz7Jh6eYEf93S7vA2za6xM7QT85L4+VY=
github.com/gobuffalo/packr/v2 v2.8.3 h1:xE1yzvnO56cUC0sTpKR3DIbxZgB54AftTFMhB2XEWlY=
github.com/gobuffalo/packr/v2 v2.8.3/go.mod h1:0SahksCVcx4IMnigTjiFuyldmTrdTctXsOdiU5KwbKc=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godror/godror v0.24.2/go.mod h1:wZv/9vPiUib6tkoDl+AZ/QLf5YZgMravZ7jxH2eQWAE=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/securecookie v1.1.2-0.20180608144417-78f3d318a8bf h1:tgJ+TrHb8GE+aKrvU3dWpVEByJtj9/i7QRMVtX1XMmc=
github.com/gorilla/securecookie v1.1.2-0.20180608144417-78f3d318a8bf/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw=
github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kortschak/utter v1.0.1/go.mod h1:vSmSjbyrlKjjsL71193LmzBOKgwePk9DH6uFaWHIInc=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/markbates/errx v1.1.0 h1:QDFeR+UP95dO12JgW+tgi2UVfo0V8YBHiUIOaeBPiEI=
github.com/markbates/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc=
github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY=
github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI=
github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI=
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-oci8 v0.1.1/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/cli v1.1.4/go.mod h1:vTLESy5mRhKOs9KDp0/RATawxP1UqBmdrpVRMnpcvKQ=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mssola/user_agent v0.5.3 h1:lBRPML9mdFuIZgI2cmlQ+atbpJdLdeVl2IDodjBR578=
github.com/mssola/user_agent v0.5.3/go.mod h1:TTPno8LPY3wAIEKRpAtkdMT0f8SE24pLRGPahjCH4uw=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/poy/onpar v0.0.0-20190519213022-ee068f8ea4d1 h1:oL4IBbcqwhhNWh31bjOX8C/OCy0zs9906d/VUru+bqg=
github.com/poy/onpar v0.0.0-20190519213022-ee068f8ea4d1/go.mod h1:nSbFQvMj97ZyhFRSJYtut+msi4sOY6zJDGCdSc+/rZU=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rubenv/sql-migrate v1.2.0 h1:fOXMPLMd41sK7Tg75SXDec15k3zg5WNV6SjuDRiNfcU=
github.com/rubenv/sql-migrate v1.2.0/go.mod h1:Z5uVnq7vrIrPmHbVFfR4YLHRZquxeHpckCnRq0P/K9Y=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/urfave/cli v1.22.10 h1:p8Fspmz3iTctJstry1PYS3HVdllxnEzTEsgIgtxTrCk=
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
github.com/ziutek/mymysql v1.5.5-0.20171217234033-ff6cc86d3d93 h1:neEPpeeJDl7Rs0FRhSyk0SloWCRoNdwn4wB6nD4eP7Q=
github.com/ziutek/mymysql v1.5.5-0.20171217234033-ff6cc86d3d93/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=


================================================
FILE: gulpfile.js
================================================
'use strict';

const browserify = require('browserify')
const gulp = require('gulp')
const source = require('vinyl-source-stream')
const buffer = require('vinyl-buffer')
const uglify = require('gulp-uglify')
const babel = require('gulp-babel');
const cachebust = require('gulp-cache-bust');
const concat = require('gulp-concat');
const gulpif = require('gulp-if')

const debug = process.env.NODE_ENV !== 'production';

gulp.task('app-js', function () {
    return browserify({
        entries: './assets/src/js/script.js',
        debug: debug,
        ignoreMissing: true,
    })
    .transform("babelify", {
      presets: ["@babel/preset-env"],
      plugins: [ 
        ["@babel/plugin-proposal-decorators", { "legacy": true }],
        ["@babel/plugin-transform-react-jsx", { "pragma":"h" } ]
      ]
    })    
    .bundle()  
    .pipe(source('script.js'))
	.pipe(buffer())
	.pipe(gulpif(!debug, uglify()))
  	.pipe(gulp.dest(`./assets/build/js`))  
});  

gulp.task('tracker-js', function () {
  return gulp.src('./assets/src/js/tracker.js')
        .pipe(babel({
            presets: ["@babel/preset-env"],
        }))
        .pipe(gulpif(!debug, uglify()))
        .pipe(gulp.dest('./assets/build/js'));
});

gulp.task('fonts', function() {
  return gulp.src('./assets/src/fonts/**/*')
    .pipe(gulp.dest(`./assets/build/fonts`))
});

gulp.task('img', function() {
  return gulp.src('./assets/src/img/**/*')
    .pipe(gulp.dest(`./assets/build/img`))
});

gulp.task('html', function() {
  return gulp.src('./assets/src/**/*.html')
    .pipe(cachebust({
      type: 'timestamp'
    }))
    .pipe(gulp.dest(`./assets/build/`))
});

gulp.task('css', function () {
	return gulp.src('./assets/src/css/*.css')
		.pipe(concat('styles.css'))
		.pipe(gulp.dest(`./assets/build/css`))
});

gulp.task('default', gulp.series('app-js', 'tracker-js', 'css', 'html', 'img', 'fonts' ) );

gulp.task('watch', gulp.series('default', function() {
  gulp.watch(['./assets/src/js/**/*.js'], gulp.parallel('app-js', 'tracker-js') );
  gulp.watch(['./assets/src/css/**/*.css'], gulp.parallel( 'css') );
  gulp.watch(['./assets/src/**/*.html'], gulp.parallel( 'html') );
  gulp.watch(['./assets/src/img/**/*'], gulp.parallel( 'img') );
  gulp.watch(['./assets/src/fonts/**/*'], gulp.parallel( 'fonts') );
}));


================================================
FILE: main.go
================================================
package main

import (
	"fmt"
	"os"

	"github.com/usefathom/fathom/pkg/cli"
)

var (
	version = "dev"
	commit  = "none"
	date    = "unknown"
)

func main() {
	err := cli.Run(version, commit, date)
	if err != nil {
		fmt.Print(err)
		os.Exit(1)
	}

	os.Exit(0)
}


================================================
FILE: package.json
================================================
{
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/usefathom/fathom.git"
  },
  "devDependencies": {
    "@babel/core": "^7.0.1",
    "@babel/plugin-proposal-decorators": "^7.0.0",
    "@babel/plugin-transform-react-jsx": "^7.0.0",
    "@babel/preset-env": "^7.0.0",
    "babelify": "^10.0.0",
    "browserify": "^16.2.0",
    "gulp": "^4.0.0",
    "gulp-babel": "^8.0.0",
    "gulp-cache-bust": "^1.4.0",
    "gulp-concat": "^2.6.1",
    "gulp-if": "^2.0.2",
    "gulp-uglify": "^3.0.0",
    "vinyl-buffer": "^1.0.0",
    "vinyl-source-stream": "^2.0.0"
  },
  "dependencies": {
    "classnames": "^2.2.6",
    "d3": "^5.7.0",
    "d3-tip": "^0.9.1",
    "d3-transition": "^1.1.3",
    "decko": "^1.2.0",
    "pikaday": "^1.8.0",
    "preact": "^8.3.1"
  }
}


================================================
FILE: pkg/aggregator/aggregator.go
================================================
package aggregator

import (
	"errors"
	"net/url"
	"strings"
	"time"

	"github.com/usefathom/fathom/pkg/datastore"
	"github.com/usefathom/fathom/pkg/models"

	log "github.com/sirupsen/logrus"
)

type Aggregator struct {
	database datastore.Datastore
}

type Report struct {
	Processed int
	PoolEmpty bool
	Duration  time.Duration
}

type results struct {
	Sites     map[string]*models.SiteStats
	Pages     map[string]*models.PageStats
	Referrers map[string]*models.ReferrerStats
}

// New returns a new aggregator instance with the database dependency injected.
func New(db datastore.Datastore) *Aggregator {
	return &Aggregator{
		database: db,
	}
}

// Run processes the pageviews which are ready to be processed and adds them to daily aggregation
func (agg *Aggregator) Run() Report {
	startTime := time.Now()

	// Get unprocessed pageviews
	limit := 10000
	pageviews, err := agg.database.GetProcessablePageviews(limit)
	emptyReport := Report{
		Processed: 0,
		PoolEmpty: true,
	}
	if err != nil && err != datastore.ErrNoResults {
		log.Error(err)
		return emptyReport
	}

	//  Do we have anything to process?
	n := len(pageviews)
	if n == 0 {
		return emptyReport
	}

	results := &results{
		Sites:     map[string]*models.SiteStats{},
		Pages:     map[string]*models.PageStats{},
		Referrers: map[string]*models.ReferrerStats{},
	}

	sites, err := agg.database.GetSites()
	if err != nil {
		log.Error(err)
		return emptyReport
	}

	// create map of public tracking ID's => site ID
	trackingIDMap := make(map[string]int64, len(sites)+1)
	for _, s := range sites {
		trackingIDMap[s.TrackingID] = s.ID
	}

	// if no explicit site ID was given in the tracking request, default to site with ID 1
	trackingIDMap[""] = 1

	// setup referrer spam blacklist
	blacklist, err := newBlacklist()
	if err != nil {
		log.Error(err)
		return emptyReport
	}

	// add each pageview to the various statistics we gather
	for _, p := range pageviews {
		// discard pageview if site tracking ID is unknown
		siteID, ok := trackingIDMap[p.SiteTrackingID]
		if !ok {
			log.Debugf("Skipping pageview because of unrecognized site tracking ID %s", p.SiteTrackingID)
			continue
		}

		// start with referrer because we may want to skip this pageview altogether if it is referrer spam
		if p.Referrer != "" {
			ref, err := parseReferrer(p.Referrer)
			if err != nil {
				log.Debugf("Skipping pageview from referrer %s because of malformed referrer URL", p.Referrer)
				continue
			}

			// ignore out pageviews from blacklisted referrers
			// we use Hostname() here to discard port numbers
			if blacklist.Has(ref.Hostname()) {
				log.Debugf("Skipping pageview from referrer %s because of blacklist", p.Referrer)
				continue
			}

			hostname := ref.Scheme + "://" + ref.Host
			referrerStats, err := agg.getReferrerStats(results, siteID, p.Timestamp, hostname, ref.Path)
			if err != nil {
				log.Error(err)
				continue
			}
			referrerStats.HandlePageview(p)
		}

		// get existing site stats so we can add this pageview to it
		site, err := agg.getSiteStats(results, siteID, p.Timestamp)
		if err != nil {
			log.Error(err)
			continue
		}
		site.HandlePageview(p)

		pageStats, err := agg.getPageStats(results, siteID, p.Timestamp, p.Hostname, p.Pathname)
		if err != nil {
			log.Error(err)
			continue
		}
		pageStats.HandlePageview(p)
	}

	// update stats
	for _, site := range results.Sites {
		if err := agg.database.SaveSiteStats(site); err != nil {
			log.Error(err)
		}
	}

	for _, pageStats := range results.Pages {
		if err := agg.database.SavePageStats(pageStats); err != nil {
			log.Error(err)
		}
	}

	for _, referrerStats := range results.Referrers {
		if err := agg.database.SaveReferrerStats(referrerStats); err != nil {
			log.Error(err)
		}
	}

	// finally, remove pageviews that we just processed
	if err := agg.database.DeletePageviews(pageviews); err != nil {
		log.Error(err)
	}

	endTime := time.Now()
	dur := endTime.Sub(startTime)

	report := Report{
		Processed: n,
		PoolEmpty: n < limit,
		Duration:  dur,
	}
	log.Debugf("processed %d pageviews. took: %s, pool empty: %v", report.Processed, report.Duration, report.PoolEmpty)
	return report
}

// parseReferrer parses the referrer string & normalizes it
func parseReferrer(r string) (*url.URL, error) {
	u, err := url.Parse(r)
	if err != nil {
		return nil, err
	}

	// always require a hostname
	if u.Host == "" {
		return nil, errors.New("malformed URL, empty host")
	}

	// remove AMP & UTM vars
	if u.RawQuery != "" {
		q := u.Query()
		keys := []string{"amp", "utm_campaign", "utm_medium", "utm_source"}
		for _, k := range keys {
			q.Del(k)
		}
		u.RawQuery = q.Encode()
	}

	// remove amp/ suffix (but keep trailing slash)
	if strings.HasSuffix(u.Path, "/amp/") {
		u.Path = u.Path[0:(len(u.Path) - 4)]
	}

	// re-parse our normalized string into a new URL struct
	return url.Parse(u.String())
}


================================================
FILE: pkg/aggregator/aggregator_test.go
================================================
package aggregator

import (
	"net/url"
	"testing"
)

func TestParseReferrer(t *testing.T) {
	testsValid := map[string]*url.URL{
		"https://www.usefathom.com/?utm_source=github": &url.URL{
			Scheme: "https",
			Host:   "www.usefathom.com",
			Path:   "/",
		},
		"https://www.usefathom.com/privacy/amp/?utm_source=github": &url.URL{
			Scheme: "https",
			Host:   "www.usefathom.com",
			Path:   "/privacy/",
		},
	}
	testsErr := []string{
		"mysite.com",
		"foobar",
		"",
	}

	for r, e := range testsValid {
		v, err := parseReferrer(r)
		if err != nil {
			t.Error(err)
		}

		if v.Host != e.Host {
			t.Errorf("Invalid Host: expected %s, got %s", e.Host, v.Host)
		}

		if v.Scheme != e.Scheme {
			t.Errorf("Invalid Scheme: expected %s, got %s", e.Scheme, v.Scheme)
		}

		if v.Path != e.Path {
			t.Errorf("Invalid Path: expected %s, got %s", e.Path, v.Path)
		}

	}

	for _, r := range testsErr {
		v, err := parseReferrer(r)
		if err == nil {
			t.Errorf("Expected err, got %#v", v)
		}
	}

}


================================================
FILE: pkg/aggregator/bindata.go
================================================
// Code generated by go-bindata.
// sources:
// pkg/aggregator/data/blacklist.txt
// DO NOT EDIT!

package aggregator

import (
	"bytes"
	"compress/gzip"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"
	"time"
)

func bindataRead(data []byte, name string) ([]byte, error) {
	gz, err := gzip.NewReader(bytes.NewBuffer(data))
	if err != nil {
		return nil, fmt.Errorf("Read %q: %v", name, err)
	}

	var buf bytes.Buffer
	_, err = io.Copy(&buf, gz)
	clErr := gz.Close()

	if err != nil {
		return nil, fmt.Errorf("Read %q: %v", name, err)
	}
	if clErr != nil {
		return nil, err
	}

	return buf.Bytes(), nil
}

type asset struct {
	bytes []byte
	info  os.FileInfo
}

type bindataFileInfo struct {
	name    string
	size    int64
	mode    os.FileMode
	modTime time.Time
}

func (fi bindataFileInfo) Name() string {
	return fi.name
}
func (fi bindataFileInfo) Size() int64 {
	return fi.size
}
func (fi bindataFileInfo) Mode() os.FileMode {
	return fi.mode
}
func (fi bindataFileInfo) ModTime() time.Time {
	return fi.modTime
}
func (fi bindataFileInfo) IsDir() bool {
	return false
}
func (fi bindataFileInfo) Sys() interface{} {
	return nil
}

var _blacklistTxt = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x84\x5b\xfb\x72\xe3\xae\xaf\xff\x3f\xef\x42\xa6\xed\x76\x2f\xdf\xc7\x91\x41\xb6\x55\x2e\x62\xb9\x38\x71\x9e\xfe\x8c\x00\xa7\xe9\xb6\xa7\xbf\x9d\xe9\x2c\xba\x18\x63\x01\xe2\x23\x89\x3c\xfd\xc0\x33\x85\x99\x4f\x4f\x41\x39\x0a\x78\x2e\xdb\xe9\x59\xfd\xf7\x5f\x46\x3e\x6b\xf6\xa7\x67\x35\x27\x44\x95\x57\x48\xa8\xa6\x5a\x0a\x87\xdc\x05\x4f\x4f\x86\x9d\x83\x94\xd5\x5d\xf7\xe9\x29\x23\x24\xbd\x62\x58\x28\xe0\xd0\x7b\xf1\x90\x0b\x26\xde\x06\x59\xfb\xfb\x9e\x23\x78\x7f\x4e\xf5\xf4\x7c\xc1\xa9\xab\x9c\xbd\x3b\xbd\xbc\x5e\x7f\xab\x8c\x69\xc3\xa4\x72\x8d\x91\x53\x39\x67\x2a\x78\x7a\xd9\xb9\xa6\xde\xfc\xa1\x1c\x96\x82\x49\x19\xf6\x40\x21\x9f\x03\x96\xd3\x8f\x0b\xec\x01\xcb\x85\x93\xed\x2f\x7e\xa5\x10\xa4\xff\x57\xca\x85\xf3\xca\xb1\x73\xef\x6f\xcb\x67\x4e\xcb\xe9\xa7\xca\x05\x63\x56\x85\x55\x2e\x90\x8a\x9a\x6a\x96\xb1\xf7\x3e\x7e\xce\x9c\xf0\x2a\xbd\xfc\x5a\x39\x46\x0a\x4b\x63\xff\xb6\x1c\x85\xf9\xdb\x83\x45\xcf\x01\x77\x0e\xcd\x78\x4d\x78\x83\xfe\x2a\x98\xb4\xc1\x79\x3d\x5f\xf7\x5b\x6f\x2f\x47\xd3\x41\x0d\x7a\xc5\xd4\xd5\x34\x98\xfe\x09\xa0\x21\x25\x64\xe9\x19\x74\x0d\x58\xe8\xaa\x12\xce\x98\xd2\xa1\x6a\x20\xd0\x3a\x9a\x1a\xf2\xd1\x9c\xcf\x6e\x3f\x81\xc9\x11\x52\x19\xac\x42\x0b\xa6\x73\xb1\x27\x30\x1b\x86\x52\x13\x46\x48\x56\xb3\x7c\x24\x69\x18\x5a\x1b\x69\x9c\x39\x55\xdf\xe7\x04\xcc\xc6\x16\xca\x31\x50\x4c\x6c\xe8\x06\x14\xba\xf6\x3c\x73\x32\x30\x39\xbc\xe0\x24\xf3\x90\x21\x18\xcf\x13\x39\x84\x18\xf3\xd0\xe1\x04\x6d\xfc\x34\xb1\xa3\x32\xde\x43\x06\x92\xc7\x09\xdd\xd9\xde\x4e\x60\xeb\x1a\xe8\x3c\xed\x27\x70\x33\x4c\x5c\xfa\xcb\x1c\x4d\x98\x4b\x06\xd7\xad\x08\x8e\xf0\xda\x28\xe9\xcd\x51\x80\x09\x02\xa1\xb6\x70\x8e\xee\x04\xce\x42\x98\x21\x79\x68\x93\x08\xce\x61\x5a\x48\xdb\xf1\x68\xa3\xde\xe0\xa0\x6c\xe0\xcb\xf8\x40\xe7\x3c\x24\x8b\x25\x07\xbc\x18\xd8\x1d\x9d\x17\x13\x84\x1d\xf0\x92\xcf\xde\x1c\xcd\x97\xd7\x33\x35\xfe\x85\x3d\x86\xfb\xc3\x97\x44\xcb\x2a\x2b\xb5\x0d\x2a\xae\x6d\x04\x6d\xea\x5c\xc1\xe4\xe9\x7a\xae\x70\x02\x0f\x37\x0e\xb2\x2b\xda\x42\x26\x3d\xbe\xc8\x17\x65\xdb\x83\xbe\xa0\x53\x5b\x42\x83\xb9\xe0\x61\xdd\x00\x4e\x81\x4e\x3c\x41\xc9\xe7\x95\xdd\x19\x73\x63\xee\x85\x74\x56\xb2\x44\x9a\x99\x02\x44\x50\x14\x42\x6e\x3d\x05\x93\x98\x8c\xca\x65\x3f\xcc\x16\xc8\xcb\xc0\xb8\xf0\xf1\x00\x79\x94\xbf\xae\x5f\x48\x27\xb8\x38\x4c\xdd\x6e\xa1\xd0\x52\x61\x82\x34\x55\xd3\xe7\x2d\x16\xb4\xa0\xda\x97\x35\x3a\x61\x30\x60\x37\x48\x85\x52\x9b\x3d\x61\xf0\x26\x13\xd0\xfb\x4f\xd6\xd2\xc6\xae\x14\xea\x86\x48\xc5\xa0\xe5\x61\xb1\x54\x22\x50\x28\xaa\x1b\xbd\x77\x58\x22\xa4\xbf\xf5\xa0\x6a\x74\x62\xd2\x63\x67\x41\xb6\x6a\xaf\xa3\x5d\x1c\x16\xd2\x7d\x2c\x6d\xb8\x65\xb7\xfd\xbb\x6b\x61\xa5\xd9\x47\x87\xd7\xb6\x90\x84\xb6\x04\x6a\xae\xce\x99\x44\x1b\xde\xb5\x1e\x67\xa1\xf5\x51\x0b\x4f\x8e\x17\x21\xda\x64\xd5\xc2\x5f\xe9\x08\xaf
Download .txt
gitextract_bylwszv8/

├── .env.example
├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       ├── goimports.yml
│       ├── release.yml
│       └── test.yml
├── .gitignore
├── .goreleaser.yml
├── CODE_OF_CONDUCT.md
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── assets/
│   └── src/
│       ├── 404.html
│       ├── css/
│       │   ├── chart.css
│       │   ├── fonts-overpass.css
│       │   ├── pikaday.css
│       │   ├── styles.css
│       │   └── util.css
│       ├── index.html
│       └── js/
│           ├── components/
│           │   ├── Chart.js
│           │   ├── CountWidget.js
│           │   ├── DatePicker.js
│           │   ├── Gearwheel.js
│           │   ├── LoginForm.js
│           │   ├── LogoutButton.js
│           │   ├── Notification.js
│           │   ├── Pikadayer.js
│           │   ├── Realtime.js
│           │   ├── Sidebar.js
│           │   ├── SiteSettings.js
│           │   ├── SiteSwitcher.js
│           │   └── Table.js
│           ├── lib/
│           │   ├── client.js
│           │   ├── numbers.js
│           │   └── util.js
│           ├── pages/
│           │   ├── dashboard.js
│           │   └── login.js
│           ├── script.js
│           └── tracker.js
├── docker-compose.yml
├── docs/
│   ├── Configuration.md
│   ├── FAQ.md
│   ├── Installation instructions.md
│   ├── README.md
│   ├── Updating to the latest version.md
│   └── misc/
│       ├── Heroku.md
│       ├── NGINX.md
│       └── Systemd.md
├── go.mod
├── go.sum
├── gulpfile.js
├── main.go
├── package.json
└── pkg/
    ├── aggregator/
    │   ├── aggregator.go
    │   ├── aggregator_test.go
    │   ├── bindata.go
    │   ├── blacklist.go
    │   ├── blacklist_test.go
    │   ├── data/
    │   │   └── blacklist.txt
    │   └── store.go
    ├── api/
    │   ├── api.go
    │   ├── auth.go
    │   ├── auth_test.go
    │   ├── collect.go
    │   ├── collect_test.go
    │   ├── health.go
    │   ├── http.go
    │   ├── http_test.go
    │   ├── page_stats.go
    │   ├── params.go
    │   ├── params_test.go
    │   ├── referrer_stats.go
    │   ├── routes.go
    │   ├── site_stats.go
    │   └── sites.go
    ├── cli/
    │   ├── cli.go
    │   ├── server.go
    │   ├── stats.go
    │   └── user.go
    ├── config/
    │   ├── config.go
    │   └── config_test.go
    ├── datastore/
    │   ├── datastore.go
    │   └── sqlstore/
    │       ├── config.go
    │       ├── config_test.go
    │       ├── hostnames.go
    │       ├── migrations/
    │       │   ├── mysql/
    │       │   │   ├── 10_alter_stats_table_constraints.sql
    │       │   │   ├── 11_add_pageview_finished_column.sql
    │       │   │   ├── 12_create_hostnames_table.sql
    │       │   │   ├── 13_create_unique_hostname_index.sql
    │       │   │   ├── 14_create_pathnames_table.sql
    │       │   │   ├── 15_create_unique_pathname_index.sql
    │       │   │   ├── 16_fill_hostnames_table.sql
    │       │   │   ├── 17_fill_pathnames_table.sql
    │       │   │   ├── 18_alter_page_stats_table.sql
    │       │   │   ├── 19_alter_referrer_stats_table.sql
    │       │   │   ├── 1_initial_tables.sql
    │       │   │   ├── 20_recreate_stats_indices.sql
    │       │   │   ├── 21_alter_page_stats_table.sql
    │       │   │   ├── 22_alter_site_stats_table.sql
    │       │   │   ├── 23_alter_referrer_stats_table.sql
    │       │   │   ├── 24_recreate_stat_table_indices.sql
    │       │   │   ├── 2_known_durations_column.sql
    │       │   │   ├── 3_referrer_group_column.sql
    │       │   │   ├── 4_pageview_id_column.sql
    │       │   │   ├── 5_create_sites_table.sql
    │       │   │   ├── 6_add_site_tracking_id_column_to_pageviews_table.sql
    │       │   │   ├── 7_add_site_id_to_site_stats_table.sql
    │       │   │   ├── 8_add_site_id_to_page_stats_table.sql
    │       │   │   └── 9_add_site_id_to_referrer_stats_table.sql
    │       │   ├── postgres/
    │       │   │   ├── 10_alter_numeric_column_precision.sql
    │       │   │   ├── 11_alter_stats_table_constraints.sql
    │       │   │   ├── 12_add_pageview_finished_column.sql
    │       │   │   ├── 13_create_hostnames_table.sql
    │       │   │   ├── 14_create_unique_hostname_index.sql
    │       │   │   ├── 15_create_pathnames_table.sql
    │       │   │   ├── 16_create_unique_pathname_index.sql
    │       │   │   ├── 17_fill_hostnames_table.sql
    │       │   │   ├── 18_fill_pathnames_table.sql
    │       │   │   ├── 19_alter_page_stats_table.sql
    │       │   │   ├── 1_initial_tables.sql
    │       │   │   ├── 20_alter_referrer_stats_table.sql
    │       │   │   ├── 21_recreate_stats_indices.sql
    │       │   │   ├── 22_alter_page_stats_table.sql
    │       │   │   ├── 23_alter_referrer_stats_table.sql
    │       │   │   ├── 24_alter_site_stats_table.sql
    │       │   │   ├── 25_recreate_stat_table_indices.sql
    │       │   │   ├── 26_alter_pageviews_table.sql
    │       │   │   ├── 2_known_durations_column.sql
    │       │   │   ├── 3_referrer_group_column.sql
    │       │   │   ├── 4_pageview_id_column.sql
    │       │   │   ├── 5_create_sites_table.sql
    │       │   │   ├── 6_add_site_tracking_id_column_to_pageviews_table.sql
    │       │   │   ├── 7_add_site_id_to_site_stats_table.sql
    │       │   │   ├── 8_add_site_id_to_page_stats_table.sql
    │       │   │   └── 9_add_site_id_to_referrer_stats_table.sql
    │       │   └── sqlite3/
    │       │       ├── 10_alter_stats_table_constraints.sql
    │       │       ├── 11_add_pageview_finished_column.sql
    │       │       ├── 12_create_hostnames_table.sql
    │       │       ├── 13_create_unique_hostname_index.sql
    │       │       ├── 14_create_pathnames_table.sql
    │       │       ├── 15_create_unique_pathname_index.sql
    │       │       ├── 15_vacuum.sql
    │       │       ├── 16_fill_hostnames_table.sql
    │       │       ├── 17_fill_pathnames_table.sql
    │       │       ├── 18_alter_page_stats_table.sql
    │       │       ├── 19_alter_referrer_stats_table.sql
    │       │       ├── 1_initial_tables.sql
    │       │       ├── 20_recreate_stats_indices.sql
    │       │       ├── 21_alter_page_stats_table.sql
    │       │       ├── 22_alter_site_stats_table.sql
    │       │       ├── 23_alter_referrer_stats_table.sql
    │       │       ├── 24_recreate_stat_table_indices.sql
    │       │       ├── 25_vacuum.sql
    │       │       ├── 26_sites_id_autoinc.sql
    │       │       ├── 2_known_durations_column.sql
    │       │       ├── 3_referrer_group_column.sql
    │       │       ├── 4_pageview_id_column.sql
    │       │       ├── 5_create_sites_table.sql
    │       │       ├── 6_add_site_tracking_id_column_to_pageviews_table.sql
    │       │       ├── 7_add_site_id_to_site_stats_table.sql
    │       │       ├── 8_add_site_id_to_page_stats_table.sql
    │       │       └── 9_add_site_id_to_referrer_stats_table.sql
    │       ├── page_stats.go
    │       ├── pageviews.go
    │       ├── pathnames.go
    │       ├── referrer_stats.go
    │       ├── seed/
    │       │   └── pageviews.sql
    │       ├── site_stats.go
    │       ├── sites.go
    │       ├── sqlstore.go
    │       └── users.go
    └── models/
        ├── page_stats.go
        ├── page_stats_test.go
        ├── pageview.go
        ├── referrer_stats.go
        ├── referrer_stats_test.go
        ├── site.go
        ├── site_stats.go
        ├── site_stats_test.go
        ├── user.go
        └── user_test.go
Download .txt
SYMBOL INDEX (409 symbols across 118 files)

FILE: assets/src/js/components/Chart.js
  function xTickFormat (line 16) | function xTickFormat(tickStep, n) {
  function incrementDate (line 56) | function incrementDate(date, incr) {
  class Chart (line 60) | class Chart extends Component {
    method constructor (line 61) | constructor(props) {
    method componentWillReceiveProps (line 72) | componentWillReceiveProps(newProps) {
    method chartData (line 88) | @bind
    method prepareChart (line 146) | @bind
    method redrawChart (line 194) | @bind
    method fetchData (line 274) | @bind
    method render (line 294) | render(props, state) {

FILE: assets/src/js/components/CountWidget.js
  class CountWidget (line 11) | class CountWidget extends Component {
    method componentWillReceiveProps (line 12) | componentWillReceiveProps(newProps, newState) {
    method countUp (line 21) | @bind
    method formatValue (line 42) | @bind
    method render (line 66) | render(props, state) {

FILE: assets/src/js/components/DatePicker.js
  class DatePicker (line 85) | class DatePicker extends Component {
    method constructor (line 86) | constructor(props) {
    method componentDidMount (line 107) | componentDidMount() {
    method componentWillUnmount (line 111) | componentWillUnmount() {
    method updateDatesFromPeriod (line 115) | @bind
    method setDateRange (line 124) | @bind
    method calculateDiff (line 165) | calculateDiff(start, end) {
    method updateURL (line 169) | updateURL() {
    method setPeriod (line 177) | @bind
    method dateValue (line 190) | dateValue(date) {
    method setStartDate (line 194) | @bind
    method setEndDate (line 199) | @bind
    method handleKeyPress (line 204) | @bind
    method setGroupBy (line 233) | @bind
    method render (line 242) | render(props, state) {

FILE: assets/src/js/components/Gearwheel.js
  class Gearwheel (line 5) | class Gearwheel extends Component {
    method render (line 6) | render(props, state) {

FILE: assets/src/js/components/LoginForm.js
  class LoginForm (line 8) | class LoginForm extends Component {
    method constructor (line 10) | constructor(props) {
    method handleSubmit (line 19) | @bind
    method updatePassword (line 40) | @bind
    method updateEmail (line 45) | @bind
    method clearMessage (line 50) | @bind
    method render (line 55) | render(props, state) {

FILE: assets/src/js/components/LogoutButton.js
  class LogoutButton (line 7) | class LogoutButton extends Component {
    method handleSubmit (line 9) | @bind
    method render (line 18) | render() {

FILE: assets/src/js/components/Notification.js
  class Notification (line 6) | class Notification extends Component {
    method constructor (line 7) | constructor(props) {
    method componentWillReceiveProps (line 17) | componentWillReceiveProps(newProps) {
    method render (line 31) | render(props, state) {

FILE: assets/src/js/components/Pikadayer.js
  class Pikadayer (line 6) | class Pikadayer extends Component {
    method componentDidMount (line 7) | componentDidMount() {
    method componentWillReceiveProps (line 15) | componentWillReceiveProps(newProps) {
    method componentWillUnmount (line 22) | componentWillUnmount() {
    method render (line 26) | render(props) {

FILE: assets/src/js/components/Realtime.js
  class Realtime (line 8) | class Realtime extends Component {
    method constructor (line 10) | constructor(props) {
    method componentDidMount (line 18) | componentDidMount() {
    method componentWillUnmount (line 23) | componentWillUnmount() {
    method componentWillReceiveProps (line 27) | componentWillReceiveProps(newProps, newState) {
    method paramsChanged (line 35) | paramsChanged(o, n) {
    method setDocumentTitle (line 39) | @bind
    method handleIntervalEvent (line 46) | @bind
    method fetchData (line 51) | @bind
    method render (line 61) | render(props, state) {

FILE: assets/src/js/components/Sidebar.js
  class Sidebar (line 9) | class Sidebar extends Component {
    method constructor (line 10) | constructor(props) {
    method componentWillReceiveProps (line 19) | componentWillReceiveProps(newProps, newState) {
    method paramsChanged (line 27) | paramsChanged(o, n) {
    method fetchData (line 31) | @bind
    method render (line 56) | render(props, state) {

FILE: assets/src/js/components/SiteSettings.js
  method constructor (line 8) | constructor(props) {
  method componentDidMount (line 17) | componentDidMount() {
  method componentWillUnmount (line 20) | componentWillUnmount() {
  method revertTemporaryState (line 24) | @bind
  method copyToClipboard (line 32) | @bind
  method deleteSite (line 40) | @bind
  method onSubmit (line 54) | @bind
  method handleTextareaClickEvent (line 74) | @bind
  method handleClickEvent (line 79) | @bind
  method handleKeydownEvent (line 89) | @bind
  method setTextarea (line 97) | @bind
  method updateSiteName (line 102) | @bind

FILE: assets/src/js/components/SiteSwitcher.js
  function arrayToQueryString (line 9) | function arrayToQueryString(array_in){
  method constructor (line 19) | constructor() {
  method selectSite (line 26) | @bind
  method addSite (line 41) | @bind
  method expand (line 46) | @bind
  method collapse (line 53) | @bind
  method toggleExpanded (line 60) | @bind

FILE: assets/src/js/components/Table.js
  class Table (line 12) | class Table extends Component {
    method constructor (line 14) | constructor(props) {
    method componentWillReceiveProps (line 26) | componentWillReceiveProps(newProps, newState) {
    method paramsChanged (line 34) | paramsChanged(o, n) {
    method fetchData (line 38) | @bind
    method paginateNext (line 66) | @bind
    method paginatePrev (line 72) | @bind
    method render (line 82) | render(props, state) {

FILE: assets/src/js/lib/client.js
  function handleRequestErrors (line 31) | function handleRequestErrors(r) {
  function parseJSON (line 40) | function parseJSON(r) {
  function parseData (line 44) | function parseData(d) {

FILE: assets/src/js/lib/numbers.js
  function formatPretty (line 8) | function formatPretty(num) {
  function formatWithComma (line 26) | function formatWithComma(nStr) {
  function formatDuration (line 42) | function formatDuration(seconds) {
  function formatPercentage (line 49) | function formatPercentage(p) {

FILE: assets/src/js/lib/util.js
  function stringifyObject (line 4) | function stringifyObject(json) {
  function randomString (line 14) | function randomString(n) {
  function hashParams (line 19) | function hashParams() {

FILE: assets/src/js/pages/dashboard.js
  class Dashboard (line 20) | class Dashboard extends Component {
    method constructor (line 21) | constructor(props) {
    method componentDidMount (line 40) | componentDidMount() {
    method fetchSites (line 44) | @bind
    method changeDateRange (line 70) | @bind
    method showSiteSettings (line 79) | @bind
    method closeSiteSettings (line 89) | @bind
    method changeSelectedSite (line 99) | @bind
    method updateSite (line 113) | @bind
    method deleteSite (line 134) | @bind
    method render (line 144) | render(props, state) {

FILE: assets/src/js/script.js
  class App (line 9) | class App extends Component {
    method constructor (line 10) | constructor(props) {
    method fetchAuthStatus (line 20) | @bind
    method toggleAuth (line 28) | @bind
    method render (line 35) | render(props, state) {

FILE: assets/src/js/tracker.js
  function set (line 15) | function set(key, value) {
  function setTrackerUrl (line 19) | function setTrackerUrl(value) {
  function stringifyObject (line 24) | function stringifyObject(obj) {
  function getCookie (line 33) | function getCookie(name) {
  function setCookie (line 49) | function setCookie(name, data, args) {
  function newVisitorData (line 65) | function newVisitorData() {
  function getData (line 75) | function getData() {
  function findTrackerUrl (line 98) | function findTrackerUrl() {
  function trackPageview (line 103) | function trackPageview(vars) {

FILE: main.go
  function main (line 16) | func main() {

FILE: pkg/aggregator/aggregator.go
  type Aggregator (line 15) | type Aggregator struct
    method Run (line 39) | func (agg *Aggregator) Run() Report {
  type Report (line 19) | type Report struct
  type results (line 25) | type results struct
  function New (line 32) | func New(db datastore.Datastore) *Aggregator {
  function parseReferrer (line 174) | func parseReferrer(r string) (*url.URL, error) {

FILE: pkg/aggregator/aggregator_test.go
  function TestParseReferrer (line 8) | func TestParseReferrer(t *testing.T) {

FILE: pkg/aggregator/bindata.go
  function bindataRead (line 20) | func bindataRead(data []byte, name string) ([]byte, error) {
  type asset (line 40) | type asset struct
  type bindataFileInfo (line 45) | type bindataFileInfo struct
    method Name (line 52) | func (fi bindataFileInfo) Name() string {
    method Size (line 55) | func (fi bindataFileInfo) Size() int64 {
    method Mode (line 58) | func (fi bindataFileInfo) Mode() os.FileMode {
    method ModTime (line 61) | func (fi bindataFileInfo) ModTime() time.Time {
    method IsDir (line 64) | func (fi bindataFileInfo) IsDir() bool {
    method Sys (line 67) | func (fi bindataFileInfo) Sys() interface{} {
  function blacklistTxtBytes (line 73) | func blacklistTxtBytes() ([]byte, error) {
  function blacklistTxt (line 80) | func blacklistTxt() (*asset, error) {
  function Asset (line 94) | func Asset(name string) ([]byte, error) {
  function MustAsset (line 108) | func MustAsset(name string) []byte {
  function AssetInfo (line 120) | func AssetInfo(name string) (os.FileInfo, error) {
  function AssetNames (line 133) | func AssetNames() []string {
  function AssetDir (line 159) | func AssetDir(name string) ([]string, error) {
  type bintree (line 181) | type bintree struct
  function RestoreAsset (line 191) | func RestoreAsset(dir, name string) error {
  function RestoreAssets (line 216) | func RestoreAssets(dir, name string) error {
  function _filePath (line 232) | func _filePath(dir, name string) string {

FILE: pkg/aggregator/blacklist.go
  type blacklist (line 9) | type blacklist struct
    method Has (line 26) | func (b *blacklist) Has(r string) bool {
  function newBlacklist (line 13) | func newBlacklist() (*blacklist, error) {

FILE: pkg/aggregator/blacklist_test.go
  function TestBlacklistHas (line 7) | func TestBlacklistHas(t *testing.T) {

FILE: pkg/aggregator/store.go
  method getSiteStats (line 12) | func (agg *Aggregator) getSiteStats(r *results, siteID int64, t time.Tim...
  method getPageStats (line 37) | func (agg *Aggregator) getPageStats(r *results, siteID int64, t time.Tim...
  method getReferrerStats (line 73) | func (agg *Aggregator) getReferrerStats(r *results, siteID int64, t time...

FILE: pkg/api/api.go
  type API (line 8) | type API struct
  function New (line 14) | func New(db datastore.Datastore, secret string) *API {

FILE: pkg/api/auth.go
  type key (line 12) | type key
  constant userKey (line 15) | userKey key = 0
  type login (line 18) | type login struct
    method Sanitize (line 23) | func (l *login) Sanitize() {
  method GetSession (line 28) | func (api *API) GetSession(w http.ResponseWriter, r *http.Request) error {
  method CreateSession (line 50) | func (api *API) CreateSession(w http.ResponseWriter, r *http.Request) er...
  method DeleteSession (line 82) | func (api *API) DeleteSession(w http.ResponseWriter, r *http.Request) er...
  method Authorize (line 96) | func (api *API) Authorize(next http.Handler) http.Handler {

FILE: pkg/api/auth_test.go
  function TestLoginSanitize (line 5) | func TestLoginSanitize(t *testing.T) {

FILE: pkg/api/collect.go
  type Collector (line 19) | type Collector struct
    method ServeHTTP (line 47) | func (c *Collector) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    method aggregate (line 108) | func (c *Collector) aggregate() {
    method worker (line 129) | func (c *Collector) worker(cap int, timeout time.Duration) {
    method buffer (line 148) | func (c *Collector) buffer(p *models.Pageview) int {
    method persist (line 160) | func (c *Collector) persist() {
  function NewCollector (line 30) | func NewCollector(store datastore.Datastore) *Collector {
  function shouldCollect (line 180) | func shouldCollect(r *http.Request) bool {
  function parsePathname (line 209) | func parsePathname(p string) string {
  function parseHostname (line 213) | func parseHostname(r string) string {

FILE: pkg/api/collect_test.go
  function TestShouldCollect (line 8) | func TestShouldCollect(t *testing.T) {
  function TestParsePathname (line 17) | func TestParsePathname(t *testing.T) {
  function TestParseHostname (line 30) | func TestParseHostname(t *testing.T) {

FILE: pkg/api/health.go
  method Health (line 6) | func (api *API) Health(w http.ResponseWriter, _ *http.Request) error {

FILE: pkg/api/http.go
  type Handler (line 12) | type Handler
    method ServeHTTP (line 19) | func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  type envelope (line 14) | type envelope struct
  function HandlerFunc (line 26) | func HandlerFunc(fn Handler) http.HandlerFunc {
  function HandleError (line 31) | func HandleError(w http.ResponseWriter, r *http.Request, err error) {
  function respond (line 42) | func respond(w http.ResponseWriter, statusCode int, d interface{}) error {
  function serveFileHandler (line 49) | func serveFileHandler(box *packr.Box, filename string) http.Handler {
  function serveFile (line 53) | func serveFile(box *packr.Box, filename string) Handler {
  function NotFoundHandler (line 76) | func NotFoundHandler(box *packr.Box) http.Handler {

FILE: pkg/api/http_test.go
  function TestRespond (line 10) | func TestRespond(t *testing.T) {

FILE: pkg/api/page_stats.go
  method GetAggregatedPageStatsHandler (line 8) | func (api *API) GetAggregatedPageStatsHandler(w http.ResponseWriter, r *...
  method GetAggregatedPageStatsPageviewsHandler (line 17) | func (api *API) GetAggregatedPageStatsPageviewsHandler(w http.ResponseWr...

FILE: pkg/api/params.go
  type Params (line 12) | type Params struct
  function GetRequestParams (line 21) | func GetRequestParams(r *http.Request) *Params {

FILE: pkg/api/params_test.go
  function TestGetRequestParams (line 10) | func TestGetRequestParams(t *testing.T) {

FILE: pkg/api/referrer_stats.go
  method GetAggregatedReferrerStatsHandler (line 7) | func (api *API) GetAggregatedReferrerStatsHandler(w http.ResponseWriter,...
  method GetAggregatedReferrerStatsPageviewsHandler (line 16) | func (api *API) GetAggregatedReferrerStatsPageviewsHandler(w http.Respon...

FILE: pkg/api/routes.go
  method Routes (line 10) | func (api *API) Routes() *mux.Router {
  function serveTrackerFile (line 47) | func serveTrackerFile(box *packr.Box) http.Handler {

FILE: pkg/api/site_stats.go
  method GetAggregatedSiteStatsHandler (line 8) | func (api *API) GetAggregatedSiteStatsHandler(w http.ResponseWriter, r *...
  method GetSiteStatsRealtimeHandler (line 18) | func (api *API) GetSiteStatsRealtimeHandler(w http.ResponseWriter, r *ht...
  method GetSiteStatsHandler (line 28) | func (api *API) GetSiteStatsHandler(w http.ResponseWriter, r *http.Reque...

FILE: pkg/api/sites.go
  function init (line 15) | func init() {
  method GetSitesHandler (line 20) | func (api *API) GetSitesHandler(w http.ResponseWriter, r *http.Request) ...
  method SaveSiteHandler (line 30) | func (api *API) SaveSiteHandler(w http.ResponseWriter, r *http.Request) ...
  method DeleteSiteHandler (line 63) | func (api *API) DeleteSiteHandler(w http.ResponseWriter, r *http.Request...
  function generateTrackingID (line 77) | func generateTrackingID() string {
  function randomString (line 81) | func randomString(len int) string {

FILE: pkg/cli/cli.go
  type App (line 15) | type App struct
  function Run (line 25) | func Run(version string, commit string, buildDate string) error {
  function before (line 62) | func before(c *cli.Context) error {
  function after (line 70) | func after(c *cli.Context) error {

FILE: pkg/cli/server.go
  function server (line 53) | func server(c *cli.Context) error {

FILE: pkg/cli/stats.go
  function stats (line 37) | func stats(c *cli.Context) error {

FILE: pkg/cli/user.go
  function userAdd (line 51) | func userAdd(c *cli.Context) error {
  function userDelete (line 88) | func userDelete(c *cli.Context) error {

FILE: pkg/config/config.go
  type Config (line 16) | type Config struct
  function LoadEnv (line 22) | func LoadEnv(file string) {
  function Parse (line 47) | func Parse() *Config {
  function randomString (line 84) | func randomString(len int) string {

FILE: pkg/config/config_test.go
  function TestLoadEnv (line 9) | func TestLoadEnv(t *testing.T) {
  function TestParse (line 31) | func TestParse(t *testing.T) {
  function TestDatabaseURL (line 52) | func TestDatabaseURL(t *testing.T) {
  function TestRandomString (line 69) | func TestRandomString(t *testing.T) {

FILE: pkg/datastore/datastore.go
  type Datastore (line 14) | type Datastore interface
  function New (line 64) | func New(c *sqlstore.Config) Datastore {

FILE: pkg/datastore/sqlstore/config.go
  type Config (line 10) | type Config struct
    method DSN (line 20) | func (c *Config) DSN() string {
    method Dbname (line 71) | func (c *Config) Dbname() string {

FILE: pkg/datastore/sqlstore/config_test.go
  function TestConfigDSN (line 8) | func TestConfigDSN(t *testing.T) {
  function TestConfigDbname (line 31) | func TestConfigDbname(t *testing.T) {

FILE: pkg/datastore/sqlstore/hostnames.go
  method HostnameID (line 7) | func (db *sqlstore) HostnameID(name string) (int64, error) {

FILE: pkg/datastore/sqlstore/migrations/mysql/10_alter_stats_table_constraints.sql
  type unique_daily_site_stats (line 7) | CREATE UNIQUE INDEX unique_daily_site_stats ON daily_site_stats(site_id,...
  type unique_daily_page_stats (line 8) | CREATE UNIQUE INDEX unique_daily_page_stats ON daily_page_stats(site_id,...
  type unique_daily_referrer_stats (line 9) | CREATE UNIQUE INDEX unique_daily_referrer_stats ON daily_referrer_stats(...
  type unique_daily_site_stats (line 17) | CREATE UNIQUE INDEX unique_daily_site_stats ON daily_site_stats(date)
  type unique_daily_page_stats (line 18) | CREATE UNIQUE INDEX unique_daily_page_stats ON daily_page_stats(hostname...
  type unique_daily_referrer_stats (line 19) | CREATE UNIQUE INDEX unique_daily_referrer_stats ON daily_referrer_stats(...

FILE: pkg/datastore/sqlstore/migrations/mysql/12_create_hostnames_table.sql
  type hostnames (line 2) | CREATE TABLE hostnames(

FILE: pkg/datastore/sqlstore/migrations/mysql/13_create_unique_hostname_index.sql
  type unique_hostnames_name (line 2) | CREATE UNIQUE INDEX unique_hostnames_name ON hostnames(name(100))

FILE: pkg/datastore/sqlstore/migrations/mysql/14_create_pathnames_table.sql
  type pathnames (line 2) | CREATE TABLE pathnames(

FILE: pkg/datastore/sqlstore/migrations/mysql/15_create_unique_pathname_index.sql
  type unique_pathnames_name (line 2) | CREATE UNIQUE INDEX unique_pathnames_name ON pathnames(name(100))

FILE: pkg/datastore/sqlstore/migrations/mysql/18_alter_page_stats_table.sql
  type daily_page_stats (line 4) | CREATE TABLE daily_page_stats(

FILE: pkg/datastore/sqlstore/migrations/mysql/19_alter_referrer_stats_table.sql
  type daily_referrer_stats (line 4) | CREATE TABLE daily_referrer_stats(

FILE: pkg/datastore/sqlstore/migrations/mysql/1_initial_tables.sql
  type users (line 3) | CREATE TABLE users (
  type pageviews (line 9) | CREATE TABLE pageviews(
  type daily_page_stats (line 23) | CREATE TABLE daily_page_stats(
  type daily_site_stats (line 34) | CREATE TABLE daily_site_stats(
  type daily_referrer_stats (line 43) | CREATE TABLE daily_referrer_stats(
  type unique_user_email (line 52) | CREATE UNIQUE INDEX unique_user_email ON users(email)
  type unique_daily_site_stats (line 53) | CREATE UNIQUE INDEX unique_daily_site_stats ON daily_site_stats(date)
  type unique_daily_page_stats (line 54) | CREATE UNIQUE INDEX unique_daily_page_stats ON daily_page_stats(hostname...
  type unique_daily_referrer_stats (line 55) | CREATE UNIQUE INDEX unique_daily_referrer_stats ON daily_referrer_stats(...

FILE: pkg/datastore/sqlstore/migrations/mysql/20_recreate_stats_indices.sql
  type unique_daily_page_stats (line 2) | CREATE UNIQUE INDEX unique_daily_page_stats ON daily_page_stats(site_id,...
  type unique_daily_referrer_stats (line 3) | CREATE UNIQUE INDEX unique_daily_referrer_stats ON daily_referrer_stats(...

FILE: pkg/datastore/sqlstore/migrations/mysql/21_alter_page_stats_table.sql
  type page_stats (line 2) | CREATE TABLE page_stats(

FILE: pkg/datastore/sqlstore/migrations/mysql/22_alter_site_stats_table.sql
  type site_stats (line 2) | CREATE TABLE site_stats(

FILE: pkg/datastore/sqlstore/migrations/mysql/23_alter_referrer_stats_table.sql
  type referrer_stats (line 2) | CREATE TABLE referrer_stats(

FILE: pkg/datastore/sqlstore/migrations/mysql/24_recreate_stat_table_indices.sql
  type unique_page_stats (line 2) | CREATE UNIQUE INDEX unique_page_stats ON page_stats(site_id, hostname_id...
  type unique_referrer_stats (line 3) | CREATE UNIQUE INDEX unique_referrer_stats ON referrer_stats(site_id, hos...
  type unique_site_stats (line 4) | CREATE UNIQUE INDEX unique_site_stats ON site_stats(site_id, ts)

FILE: pkg/datastore/sqlstore/migrations/mysql/3_referrer_group_column.sql
  type unique_daily_referrer_stats (line 13) | CREATE UNIQUE INDEX unique_daily_referrer_stats ON daily_referrer_stats(...

FILE: pkg/datastore/sqlstore/migrations/mysql/5_create_sites_table.sql
  type sites (line 2) | CREATE TABLE sites (

FILE: pkg/datastore/sqlstore/migrations/postgres/11_alter_stats_table_constraints.sql
  type unique_daily_site_stats (line 7) | CREATE UNIQUE INDEX unique_daily_site_stats ON daily_site_stats(site_id,...
  type unique_daily_page_stats (line 8) | CREATE UNIQUE INDEX unique_daily_page_stats ON daily_page_stats(site_id,...
  type unique_daily_referrer_stats (line 9) | CREATE UNIQUE INDEX unique_daily_referrer_stats ON daily_referrer_stats(...
  type unique_daily_site_stats (line 17) | CREATE UNIQUE INDEX unique_daily_site_stats ON daily_site_stats(date)
  type unique_daily_page_stats (line 18) | CREATE UNIQUE INDEX unique_daily_page_stats ON daily_page_stats(hostname...
  type unique_daily_referrer_stats (line 19) | CREATE UNIQUE INDEX unique_daily_referrer_stats ON daily_referrer_stats(...

FILE: pkg/datastore/sqlstore/migrations/postgres/13_create_hostnames_table.sql
  type hostnames (line 2) | CREATE TABLE hostnames(

FILE: pkg/datastore/sqlstore/migrations/postgres/14_create_unique_hostname_index.sql
  type unique_hostnames_name (line 2) | CREATE UNIQUE INDEX unique_hostnames_name ON hostnames(name)

FILE: pkg/datastore/sqlstore/migrations/postgres/15_create_pathnames_table.sql
  type pathnames (line 2) | CREATE TABLE pathnames(

FILE: pkg/datastore/sqlstore/migrations/postgres/16_create_unique_pathname_index.sql
  type unique_pathnames_name (line 2) | CREATE UNIQUE INDEX unique_pathnames_name ON pathnames(name)

FILE: pkg/datastore/sqlstore/migrations/postgres/19_alter_page_stats_table.sql
  type daily_page_stats (line 4) | CREATE TABLE daily_page_stats(

FILE: pkg/datastore/sqlstore/migrations/postgres/1_initial_tables.sql
  type users (line 3) | CREATE TABLE users(
  type pageviews (line 9) | CREATE TABLE pageviews(
  type daily_page_stats (line 23) | CREATE TABLE daily_page_stats(
  type daily_site_stats (line 34) | CREATE TABLE daily_site_stats(
  type daily_referrer_stats (line 43) | CREATE TABLE daily_referrer_stats(
  type unique_user_email (line 52) | CREATE UNIQUE INDEX unique_user_email ON users(email)
  type unique_daily_site_stats (line 53) | CREATE UNIQUE INDEX unique_daily_site_stats ON daily_site_stats(date)
  type unique_daily_page_stats (line 54) | CREATE UNIQUE INDEX unique_daily_page_stats ON daily_page_stats(hostname...
  type unique_daily_referrer_stats (line 55) | CREATE UNIQUE INDEX unique_daily_referrer_stats ON daily_referrer_stats(...

FILE: pkg/datastore/sqlstore/migrations/postgres/20_alter_referrer_stats_table.sql
  type daily_referrer_stats (line 4) | CREATE TABLE daily_referrer_stats(

FILE: pkg/datastore/sqlstore/migrations/postgres/21_recreate_stats_indices.sql
  type unique_daily_page_stats (line 2) | CREATE UNIQUE INDEX unique_daily_page_stats ON daily_page_stats(site_id,...
  type unique_daily_referrer_stats (line 3) | CREATE UNIQUE INDEX unique_daily_referrer_stats ON daily_referrer_stats(...

FILE: pkg/datastore/sqlstore/migrations/postgres/22_alter_page_stats_table.sql
  type page_stats (line 2) | CREATE TABLE page_stats(

FILE: pkg/datastore/sqlstore/migrations/postgres/23_alter_referrer_stats_table.sql
  type referrer_stats (line 2) | CREATE TABLE referrer_stats(

FILE: pkg/datastore/sqlstore/migrations/postgres/24_alter_site_stats_table.sql
  type site_stats (line 2) | CREATE TABLE site_stats(

FILE: pkg/datastore/sqlstore/migrations/postgres/25_recreate_stat_table_indices.sql
  type unique_page_stats (line 4) | CREATE UNIQUE INDEX unique_page_stats ON page_stats(site_id, hostname_id...
  type unique_referrer_stats (line 5) | CREATE UNIQUE INDEX unique_referrer_stats ON referrer_stats(site_id, hos...
  type unique_site_stats (line 6) | CREATE UNIQUE INDEX unique_site_stats ON site_stats(site_id, ts)

FILE: pkg/datastore/sqlstore/migrations/postgres/3_referrer_group_column.sql
  type unique_daily_referrer_stats (line 12) | CREATE UNIQUE INDEX unique_daily_referrer_stats ON daily_referrer_stats(...

FILE: pkg/datastore/sqlstore/migrations/postgres/5_create_sites_table.sql
  type sites (line 2) | CREATE TABLE sites (

FILE: pkg/datastore/sqlstore/migrations/sqlite3/10_alter_stats_table_constraints.sql
  type unique_daily_site_stats (line 7) | CREATE UNIQUE INDEX unique_daily_site_stats ON daily_site_stats(site_id,...
  type unique_daily_page_stats (line 8) | CREATE UNIQUE INDEX unique_daily_page_stats ON daily_page_stats(site_id,...
  type unique_daily_referrer_stats (line 9) | CREATE UNIQUE INDEX unique_daily_referrer_stats ON daily_referrer_stats(...
  type unique_daily_site_stats (line 17) | CREATE UNIQUE INDEX unique_daily_site_stats ON daily_site_stats(date)
  type unique_daily_page_stats (line 18) | CREATE UNIQUE INDEX unique_daily_page_stats ON daily_page_stats(hostname...
  type unique_daily_referrer_stats (line 19) | CREATE UNIQUE INDEX unique_daily_referrer_stats ON daily_referrer_stats(...

FILE: pkg/datastore/sqlstore/migrations/sqlite3/11_add_pageview_finished_column.sql
  type pageviews (line 4) | CREATE TABLE pageviews(
  type pageviews (line 22) | CREATE TABLE pageviews(

FILE: pkg/datastore/sqlstore/migrations/sqlite3/12_create_hostnames_table.sql
  type hostnames (line 2) | CREATE TABLE hostnames(

FILE: pkg/datastore/sqlstore/migrations/sqlite3/13_create_unique_hostname_index.sql
  type unique_hostnames_name (line 2) | CREATE UNIQUE INDEX unique_hostnames_name ON hostnames(name)

FILE: pkg/datastore/sqlstore/migrations/sqlite3/14_create_pathnames_table.sql
  type pathnames (line 2) | CREATE TABLE pathnames(

FILE: pkg/datastore/sqlstore/migrations/sqlite3/15_create_unique_pathname_index.sql
  type unique_pathnames_name (line 2) | CREATE UNIQUE INDEX unique_pathnames_name ON pathnames(name)

FILE: pkg/datastore/sqlstore/migrations/sqlite3/18_alter_page_stats_table.sql
  type daily_page_stats (line 4) | CREATE TABLE daily_page_stats(

FILE: pkg/datastore/sqlstore/migrations/sqlite3/19_alter_referrer_stats_table.sql
  type daily_referrer_stats (line 4) | CREATE TABLE daily_referrer_stats(

FILE: pkg/datastore/sqlstore/migrations/sqlite3/1_initial_tables.sql
  type users (line 3) | CREATE TABLE users (
  type pageviews (line 9) | CREATE TABLE pageviews(
  type daily_page_stats (line 23) | CREATE TABLE daily_page_stats(
  type daily_site_stats (line 34) | CREATE TABLE daily_site_stats(
  type daily_referrer_stats (line 43) | CREATE TABLE daily_referrer_stats(
  type unique_user_email (line 52) | CREATE UNIQUE INDEX unique_user_email ON users(email)
  type unique_daily_site_stats (line 53) | CREATE UNIQUE INDEX unique_daily_site_stats ON daily_site_stats(date)
  type unique_daily_page_stats (line 54) | CREATE UNIQUE INDEX unique_daily_page_stats ON daily_page_stats(hostname...
  type unique_daily_referrer_stats (line 55) | CREATE UNIQUE INDEX unique_daily_referrer_stats ON daily_referrer_stats(...

FILE: pkg/datastore/sqlstore/migrations/sqlite3/20_recreate_stats_indices.sql
  type unique_daily_page_stats (line 4) | CREATE UNIQUE INDEX unique_daily_page_stats ON daily_page_stats(site_id,...
  type unique_daily_referrer_stats (line 5) | CREATE UNIQUE INDEX unique_daily_referrer_stats ON daily_referrer_stats(...

FILE: pkg/datastore/sqlstore/migrations/sqlite3/21_alter_page_stats_table.sql
  type page_stats (line 2) | CREATE TABLE page_stats(

FILE: pkg/datastore/sqlstore/migrations/sqlite3/22_alter_site_stats_table.sql
  type site_stats (line 2) | CREATE TABLE site_stats(

FILE: pkg/datastore/sqlstore/migrations/sqlite3/23_alter_referrer_stats_table.sql
  type referrer_stats (line 2) | CREATE TABLE referrer_stats(

FILE: pkg/datastore/sqlstore/migrations/sqlite3/24_recreate_stat_table_indices.sql
  type unique_page_stats (line 4) | CREATE UNIQUE INDEX unique_page_stats ON page_stats(site_id, hostname_id...
  type unique_referrer_stats (line 5) | CREATE UNIQUE INDEX unique_referrer_stats ON referrer_stats(site_id, hos...
  type unique_site_stats (line 6) | CREATE UNIQUE INDEX unique_site_stats ON site_stats(site_id, ts)

FILE: pkg/datastore/sqlstore/migrations/sqlite3/26_sites_id_autoinc.sql
  type sites (line 4) | CREATE TABLE sites (
  type sites (line 14) | CREATE TABLE sites (

FILE: pkg/datastore/sqlstore/migrations/sqlite3/3_referrer_group_column.sql
  type daily_referrer_stats (line 12) | CREATE TABLE daily_referrer_stats(

FILE: pkg/datastore/sqlstore/migrations/sqlite3/4_pageview_id_column.sql
  type pageviews (line 4) | CREATE TABLE pageviews(
  type pageviews (line 20) | CREATE TABLE pageviews(

FILE: pkg/datastore/sqlstore/migrations/sqlite3/5_create_sites_table.sql
  type sites (line 2) | CREATE TABLE sites (

FILE: pkg/datastore/sqlstore/migrations/sqlite3/6_add_site_tracking_id_column_to_pageviews_table.sql
  type pageviews (line 4) | CREATE TABLE pageviews(
  type pageviews (line 22) | CREATE TABLE pageviews(

FILE: pkg/datastore/sqlstore/page_stats.go
  method GetPageStats (line 10) | func (db *sqlstore) GetPageStats(siteID int64, date time.Time, hostnameI...
  method SavePageStats (line 21) | func (db *sqlstore) SavePageStats(s *models.PageStats) error {
  method insertPageStats (line 29) | func (db *sqlstore) insertPageStats(s *models.PageStats) error {
  method updatePageStats (line 35) | func (db *sqlstore) updatePageStats(s *models.PageStats) error {
  method SelectAggregatedPageStats (line 41) | func (db *sqlstore) SelectAggregatedPageStats(siteID int64, startDate ti...
  method GetAggregatedPageStatsPageviews (line 61) | func (db *sqlstore) GetAggregatedPageStatsPageviews(siteID int64, startD...

FILE: pkg/datastore/sqlstore/pageviews.go
  method GetPageview (line 12) | func (db *sqlstore) GetPageview(id string) (*models.Pageview, error) {
  method InsertPageviews (line 26) | func (db *sqlstore) InsertPageviews(pageviews []*models.Pageview) error {
  method UpdatePageviews (line 76) | func (db *sqlstore) UpdatePageviews(pageviews []*models.Pageview) error {
  method GetProcessablePageviews (line 106) | func (db *sqlstore) GetProcessablePageviews(limit int) ([]*models.Pagevi...
  method DeletePageviews (line 114) | func (db *sqlstore) DeletePageviews(pageviews []*models.Pageview) error {

FILE: pkg/datastore/sqlstore/pathnames.go
  method PathnameID (line 7) | func (db *sqlstore) PathnameID(name string) (int64, error) {

FILE: pkg/datastore/sqlstore/referrer_stats.go
  method GetReferrerStats (line 10) | func (db *sqlstore) GetReferrerStats(siteID int64, date time.Time, hostn...
  method SaveReferrerStats (line 21) | func (db *sqlstore) SaveReferrerStats(s *models.ReferrerStats) error {
  method insertReferrerStats (line 29) | func (db *sqlstore) insertReferrerStats(s *models.ReferrerStats) error {
  method updateReferrerStats (line 35) | func (db *sqlstore) updateReferrerStats(s *models.ReferrerStats) error {
  method SelectAggregatedReferrerStats (line 41) | func (db *sqlstore) SelectAggregatedReferrerStats(siteID int64, startDat...
  method GetAggregatedReferrerStatsPageviews (line 70) | func (db *sqlstore) GetAggregatedReferrerStatsPageviews(siteID int64, st...

FILE: pkg/datastore/sqlstore/site_stats.go
  method GetSiteStats (line 11) | func (db *sqlstore) GetSiteStats(siteID int64, date time.Time) (*models....
  method SaveSiteStats (line 23) | func (db *sqlstore) SaveSiteStats(s *models.SiteStats) error {
  method insertSiteStats (line 31) | func (db *sqlstore) insertSiteStats(s *models.SiteStats) error {
  method updateSiteStats (line 37) | func (db *sqlstore) updateSiteStats(s *models.SiteStats) error {
  method SelectSiteStats (line 43) | func (db *sqlstore) SelectSiteStats(siteID int64, startDate time.Time, e...
  method GetAggregatedSiteStats (line 53) | func (db *sqlstore) GetAggregatedSiteStats(siteID int64, startDate time....
  method GetRealtimeVisitorCount (line 67) | func (db *sqlstore) GetRealtimeVisitorCount(siteID int64) (int64, error) {

FILE: pkg/datastore/sqlstore/sites.go
  method GetSites (line 10) | func (db *sqlstore) GetSites() ([]*models.Site, error) {
  method GetSite (line 23) | func (db *sqlstore) GetSite(id int64) (*models.Site, error) {
  method SaveSite (line 31) | func (db *sqlstore) SaveSite(s *models.Site) error {
  method insertSite (line 40) | func (db *sqlstore) insertSite(s *models.Site) error {
  method updateSite (line 64) | func (db *sqlstore) updateSite(s *models.Site) error {
  method DeleteSite (line 71) | func (db *sqlstore) DeleteSite(s *models.Site) error {

FILE: pkg/datastore/sqlstore/sqlstore.go
  constant MYSQL (line 19) | MYSQL    = "mysql"
  constant POSTGRES (line 20) | POSTGRES = "postgres"
  constant SQLITE (line 21) | SQLITE   = "sqlite3"
  constant DATE_FORMAT (line 23) | DATE_FORMAT = "2006-01-02 15:00:00"
  type sqlstore (line 26) | type sqlstore struct
    method Migrate (line 57) | func (db *sqlstore) Migrate() {
    method Health (line 84) | func (db *sqlstore) Health() error {
    method Close (line 92) | func (db *sqlstore) Close() error {
  function New (line 37) | func New(c *Config) *sqlstore {
  function mapError (line 96) | func mapError(err error) error {

FILE: pkg/datastore/sqlstore/users.go
  method GetUser (line 10) | func (db *sqlstore) GetUser(ID int64) (*models.User, error) {
  method GetUserByEmail (line 27) | func (db *sqlstore) GetUserByEmail(email string) (*models.User, error) {
  method SaveUser (line 35) | func (db *sqlstore) SaveUser(u *models.User) error {
  method insertUser (line 44) | func (db *sqlstore) insertUser(u *models.User) error {
  method updateUser (line 64) | func (db *sqlstore) updateUser(u *models.User) error {
  method DeleteUser (line 71) | func (db *sqlstore) DeleteUser(u *models.User) error {
  method CountUsers (line 78) | func (db *sqlstore) CountUsers() (int64, error) {

FILE: pkg/models/page_stats.go
  type PageStats (line 7) | type PageStats struct
    method HandlePageview (line 23) | func (s *PageStats) HandlePageview(p *Pageview) {

FILE: pkg/models/page_stats_test.go
  function TestPageStatsHandlePageview (line 5) | func TestPageStatsHandlePageview(t *testing.T) {

FILE: pkg/models/pageview.go
  type Pageview (line 7) | type Pageview struct

FILE: pkg/models/referrer_stats.go
  type ReferrerStats (line 7) | type ReferrerStats struct
    method HandlePageview (line 23) | func (s *ReferrerStats) HandlePageview(p *Pageview) {

FILE: pkg/models/referrer_stats_test.go
  function TestReferrerStatsHandlePageview (line 5) | func TestReferrerStatsHandlePageview(t *testing.T) {

FILE: pkg/models/site.go
  type Site (line 4) | type Site struct

FILE: pkg/models/site_stats.go
  type SiteStats (line 8) | type SiteStats struct
    method FormattedDuration (line 20) | func (s *SiteStats) FormattedDuration() string {
    method HandlePageview (line 24) | func (s *SiteStats) HandlePageview(p *Pageview) {

FILE: pkg/models/site_stats_test.go
  function TestSiteStatsFormattedDuration (line 7) | func TestSiteStatsFormattedDuration(t *testing.T) {
  function TestSiteStatsHandlePageview (line 23) | func TestSiteStatsHandlePageview(t *testing.T) {

FILE: pkg/models/user.go
  type User (line 9) | type User struct
    method SetPassword (line 25) | func (u *User) SetPassword(pwd string) {
    method ComparePassword (line 31) | func (u *User) ComparePassword(pwd string) error {
  function NewUser (line 16) | func NewUser(e string, pwd string) User {

FILE: pkg/models/user_test.go
  function TestNewUser (line 7) | func TestNewUser(t *testing.T) {
  function TestUserPassword (line 21) | func TestUserPassword(t *testing.T) {
Condensed preview — 181 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (387K chars).
[
  {
    "path": ".env.example",
    "chars": 232,
    "preview": "FATHOM_GZIP=true\nFATHOM_DEBUG=true\nFATHOM_DATABASE_DRIVER=\"sqlite3\"\nFATHOM_DATABASE_NAME=\"./fathom.db\"\nFATHOM_DATABASE_U"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 141,
    "preview": "# These are supported funding model platforms\ncustom: ['https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_butto"
  },
  {
    "path": ".github/workflows/goimports.yml",
    "chars": 403,
    "preview": "name: Check imports\non: [ push, pull_request ]\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/s"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 616,
    "preview": "name: Release\non:\n  push:\n    tags:\n      - 'v*'\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Check"
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 273,
    "preview": "---\nname: Run tests\non: [ push, pull_request ]\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions"
  },
  {
    "path": ".gitignore",
    "chars": 112,
    "preview": "node_modules\n.env*\n!.env.example\ncoverage.out\nbuild\ndist\n*.db\nfathom\n!cmd/fathom\n\nassets/build\nassets/dist\nbin/\n"
  },
  {
    "path": ".goreleaser.yml",
    "chars": 636,
    "preview": "# Documentation http://goreleaser.com\nbefore:\n  hooks:\n    - make assets/dist\n    - go install github.com/gobuffalo/pack"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 3226,
    "preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
  },
  {
    "path": "Dockerfile",
    "chars": 771,
    "preview": "FROM node:alpine AS assetbuilder\nWORKDIR /app\nCOPY package*.json ./\nCOPY gulpfile.js ./\nCOPY assets/ ./assets/\nRUN npm i"
  },
  {
    "path": "LICENSE",
    "chars": 1070,
    "preview": "MIT License\n\nCopyright (c) Conva Ventures Inc\n\nPermission is hereby granted, free of charge, to any person obtaining a c"
  },
  {
    "path": "Makefile",
    "chars": 2006,
    "preview": "EXECUTABLE := fathom\nLDFLAGS += -extldflags \"-static\" -X \"main.version=$(shell git describe --tags --always | sed 's/-/+"
  },
  {
    "path": "README.md",
    "chars": 4939,
    "preview": "Fathom Lite - simple website analytics\n==============================\n[![Go Report Card](https://goreportcard.com/badge/"
  },
  {
    "path": "assets/src/404.html",
    "chars": 550,
    "preview": "<!DOCTYPE html>\n<html class=\"no-js\" lang=\"en\">\n<head>\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=de"
  },
  {
    "path": "assets/src/css/chart.css",
    "chars": 877,
    "preview": ".box-graph {\n\tbackground: white;\n\tmargin-left: 0 !important;\n}\n\n#chart * { \n\tbox-sizing: content-box; \n}\n\n#chart .muted "
  },
  {
    "path": "assets/src/css/fonts-overpass.css",
    "chars": 6350,
    "preview": "@font-face {\n\tfont-family: 'overpass';\n\tsrc: url('../fonts/overpass-thin.eot'); /* IE9 Compat Modes */\n\tsrc: url('../fon"
  },
  {
    "path": "assets/src/css/pikaday.css",
    "chars": 3987,
    "preview": "@charset \"UTF-8\";\n\n/*!\n * Pikaday\n * Copyright © 2014 David Bushell | BSD & MIT license | http://dbushell.com/\n */\n\n.pik"
  },
  {
    "path": "assets/src/css/styles.css",
    "chars": 10968,
    "preview": "/*\noverpass    200, 500, 600\npurple      #533feb\ngreen       #88ffc6\n\ndark        #46494d\nmedium      #98a0a6\nlight     "
  },
  {
    "path": "assets/src/css/util.css",
    "chars": 1224,
    "preview": ".small-margin {\n\tmargin-top: 20px;\n\tmargin-bottom: 20px;\n}\n\n.cf:after {\n\tcontent: \"\";\n\tdisplay: table;\n\tclear: both;\n}\n\n"
  },
  {
    "path": "assets/src/index.html",
    "chars": 688,
    "preview": "<!DOCTYPE html>\n<html class=\"no-js\" lang=\"en\">\n<head>\n  <title>Fathom - simple website analytics</title>\n  <link href=\"a"
  },
  {
    "path": "assets/src/js/components/Chart.js",
    "chars": 8191,
    "preview": "'use strict';\n\nimport { h, Component } from 'preact';\nimport Client from '../lib/client.js';\nimport { bind } from 'decko"
  },
  {
    "path": "assets/src/js/components/CountWidget.js",
    "chars": 1933,
    "preview": "'use strict';\n\nimport { h, Component } from 'preact';\nimport * as numbers from '../lib/numbers.js';\nimport { bind } from"
  },
  {
    "path": "assets/src/js/components/DatePicker.js",
    "chars": 7290,
    "preview": "'use strict';\n\nimport { h, Component } from 'preact';\nimport { bind } from 'decko';\nimport Pikadayer from './Pikadayer.j"
  },
  {
    "path": "assets/src/js/components/Gearwheel.js",
    "chars": 1565,
    "preview": "'use strict';\n\nimport { h, Component } from 'preact';\n\nclass Gearwheel extends Component {\n  render(props, state) {\n    "
  },
  {
    "path": "assets/src/js/components/LoginForm.js",
    "chars": 2939,
    "preview": "'use strict';\n\nimport { h, render, Component } from 'preact';\nimport Client from '../lib/client.js';\nimport Notification"
  },
  {
    "path": "assets/src/js/components/LogoutButton.js",
    "chars": 520,
    "preview": "'use strict';\n\nimport { h, Component } from 'preact';\nimport Client from '../lib/client.js';\nimport { bind } from 'decko"
  },
  {
    "path": "assets/src/js/components/Notification.js",
    "chars": 871,
    "preview": "'use strict'\n\nimport { h, Component } from 'preact';\nimport { bind } from 'decko';\n\nclass Notification extends Component"
  },
  {
    "path": "assets/src/js/components/Pikadayer.js",
    "chars": 671,
    "preview": "'use strict';\n\nimport Pikaday from 'pikaday';\nimport { h, Component } from 'preact';\n\nclass Pikadayer extends Component "
  },
  {
    "path": "assets/src/js/components/Realtime.js",
    "chars": 1558,
    "preview": "'use strict';\n\nimport { h, Component } from 'preact';\nimport Client from '../lib/client.js';\nimport { bind } from 'decko"
  },
  {
    "path": "assets/src/js/components/Sidebar.js",
    "chars": 1872,
    "preview": "'use strict';\n\nimport { h, Component } from 'preact';\nimport Client from '../lib/client.js';\nimport { bind } from 'decko"
  },
  {
    "path": "assets/src/js/components/SiteSettings.js",
    "chars": 4628,
    "preview": "'use strict';\n\nimport { h, Component } from 'preact';\nimport Client from '../lib/client.js';\nimport { bind } from 'decko"
  },
  {
    "path": "assets/src/js/components/SiteSwitcher.js",
    "chars": 2076,
    "preview": "'use strict';\n\nimport { h, Component } from 'preact';\nimport { bind } from 'decko';\nimport { hashParams } from \"../lib/u"
  },
  {
    "path": "assets/src/js/components/Table.js",
    "chars": 3753,
    "preview": "'use strict';\n\nimport { h, Component } from 'preact';\nimport * as numbers from '../lib/numbers.js';\nimport Client from '"
  },
  {
    "path": "assets/src/js/lib/client.js",
    "chars": 1337,
    "preview": "'use strict';\n\nvar Client = {};\nClient.request = function(url, args) {\n  args = args || {};\n  args.credentials = 'same-o"
  },
  {
    "path": "assets/src/js/lib/numbers.js",
    "chars": 1195,
    "preview": "'use strict';\n\nconst M = 1000000\nconst K = 1000\nconst rx = new RegExp('\\\\.0$');\nconst commaRx = new RegExp('(\\\\d+)(\\\\d{3"
  },
  {
    "path": "assets/src/js/lib/util.js",
    "chars": 860,
    "preview": "'use strict';\n\n// convert object to query string\nfunction stringifyObject(json) {\n    var keys = Object.keys(json);\n\n   "
  },
  {
    "path": "assets/src/js/pages/dashboard.js",
    "chars": 6254,
    "preview": "'use strict'\n\nimport { h, Component } from 'preact';\nimport LogoutButton from '../components/LogoutButton.js';\nimport Re"
  },
  {
    "path": "assets/src/js/pages/login.js",
    "chars": 534,
    "preview": "'use strict';\n\nimport { h, render, Component } from 'preact';\nimport LoginForm from '../components/LoginForm.js';\n\nclass"
  },
  {
    "path": "assets/src/js/script.js",
    "chars": 902,
    "preview": "'use strict';\n\nimport { h, render, Component } from 'preact'\nimport Login from './pages/login.js'\nimport Dashboard from "
  },
  {
    "path": "assets/src/js/tracker.js",
    "chars": 5397,
    "preview": "(function() { \n  'use strict';\n\n  let queue = window.fathom.q || [];\n  let config = {\n    'siteId': '',\n    'trackerUrl'"
  },
  {
    "path": "docker-compose.yml",
    "chars": 919,
    "preview": "version: \"3\"\nservices:\n  fathom:\n    image: usefathom/fathom:latest\n    ports:\n      - \"8080:8080\"\n    environment:\n    "
  },
  {
    "path": "docs/Configuration.md",
    "chars": 2684,
    "preview": "# Configuring Fathom\n\nAll configuration in Fathom is optional. If you supply no configuration values then Fathom will de"
  },
  {
    "path": "docs/FAQ.md",
    "chars": 1751,
    "preview": "# Frequently Asked Questions\n\n### How do I install Fathom on my server?\n\nHave a look at the [installation instructions]("
  },
  {
    "path": "docs/Installation instructions.md",
    "chars": 4113,
    "preview": "# Installation instructions for Fathom\n\nTo install Fathom on your server: \n\n1. [Download the latest Fathom release](http"
  },
  {
    "path": "docs/README.md",
    "chars": 530,
    "preview": "Welcome to the fathom wiki!\n\nHow to:\n\n* [Install Fathom with our One-Click DigitalOcean installer](DigitalOcean%20One-Cl"
  },
  {
    "path": "docs/Updating to the latest version.md",
    "chars": 1229,
    "preview": "# Updating Fathom to the latest version\n\nTo update your existing Fathom installation to the latest version, first rename"
  },
  {
    "path": "docs/misc/Heroku.md",
    "chars": 2274,
    "preview": "# Running Fathom on Heroku\n\n### Requirements\n\n* heroku cli (logged in)\n* git\n* curl\n* wget\n* tar are required\n* ~ openss"
  },
  {
    "path": "docs/misc/NGINX.md",
    "chars": 1718,
    "preview": "# Using NGINX with Fathom\n\nLet's say you have the Fathom server listening on port 9000 and want to serve it on your doma"
  },
  {
    "path": "docs/misc/Systemd.md",
    "chars": 1692,
    "preview": "# Managing the Fathom process with Systemd\n\nTo run Fathom as a service (so it keeps on running in the background and is "
  },
  {
    "path": "go.mod",
    "chars": 1519,
    "preview": "module github.com/usefathom/fathom\n\ngo 1.19\n\nrequire (\n\tgithub.com/go-sql-driver/mysql v1.6.0\n\tgithub.com/gobuffalo/pack"
  },
  {
    "path": "go.sum",
    "chars": 67607,
    "preview": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1"
  },
  {
    "path": "gulpfile.js",
    "chars": 2298,
    "preview": "'use strict';\n\nconst browserify = require('browserify')\nconst gulp = require('gulp')\nconst source = require('vinyl-sourc"
  },
  {
    "path": "main.go",
    "chars": 262,
    "preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/usefathom/fathom/pkg/cli\"\n)\n\nvar (\n\tversion = \"dev\"\n\tcommit  = \"none\"\n"
  },
  {
    "path": "package.json",
    "chars": 799,
    "preview": "{\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/usefathom/fathom.git\"\n  },\n  \""
  },
  {
    "path": "pkg/aggregator/aggregator.go",
    "chars": 4874,
    "preview": "package aggregator\n\nimport (\n\t\"errors\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/usefathom/fathom/pkg/datastore\"\n\t\"git"
  },
  {
    "path": "pkg/aggregator/aggregator_test.go",
    "chars": 1002,
    "preview": "package aggregator\n\nimport (\n\t\"net/url\"\n\t\"testing\"\n)\n\nfunc TestParseReferrer(t *testing.T) {\n\ttestsValid := map[string]*"
  },
  {
    "path": "pkg/aggregator/bindata.go",
    "chars": 32486,
    "preview": "// Code generated by go-bindata.\n// sources:\n// pkg/aggregator/data/blacklist.txt\n// DO NOT EDIT!\n\npackage aggregator\n\ni"
  },
  {
    "path": "pkg/aggregator/blacklist.go",
    "chars": 734,
    "preview": "package aggregator\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"strings\"\n)\n\ntype blacklist struct {\n\tdata []byte\n}\n\nfunc newBlacklist()"
  },
  {
    "path": "pkg/aggregator/blacklist_test.go",
    "chars": 401,
    "preview": "package aggregator\n\nimport (\n\t\"testing\"\n)\n\nfunc TestBlacklistHas(t *testing.T) {\n\tb, err := newBlacklist()\n\tif err != ni"
  },
  {
    "path": "pkg/aggregator/data/blacklist.txt",
    "chars": 16104,
    "preview": "03e.info\n0n-line.tv\n1-99seo.com\n1-free-share-buttons.com\n100dollars-seo.com\n100searchengines.com\n12masterov.com\n12u.info"
  },
  {
    "path": "pkg/aggregator/store.go",
    "chars": 2959,
    "preview": "package aggregator\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/usefathom/fathom/pkg/datastore\"\n\t\"github.com/usefat"
  },
  {
    "path": "pkg/api/api.go",
    "chars": 357,
    "preview": "package api\n\nimport (\n\t\"github.com/gorilla/sessions\"\n\t\"github.com/usefathom/fathom/pkg/datastore\"\n)\n\ntype API struct {\n\t"
  },
  {
    "path": "pkg/api/auth.go",
    "chars": 3250,
    "preview": "package api\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"strings\"\n\n\tgcontext \"github.com/gorilla/context\"\n\t\"github.com/usefa"
  },
  {
    "path": "pkg/api/auth_test.go",
    "chars": 258,
    "preview": "package api\n\nimport \"testing\"\n\nfunc TestLoginSanitize(t *testing.T) {\n\trawEmail := \"Foo@foobar.com   \"\n\tl := &login{\n\t\tE"
  },
  {
    "path": "pkg/api/collect.go",
    "chars": 5266,
    "preview": "package api\n\nimport (\n\t\"encoding/base64\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\tlog \"gith"
  },
  {
    "path": "pkg/api/collect_test.go",
    "chars": 1075,
    "preview": "package api\n\nimport (\n\t\"net/http\"\n\t\"testing\"\n)\n\nfunc TestShouldCollect(t *testing.T) {\n\tr, _ := http.NewRequest(\"GET\", \""
  },
  {
    "path": "pkg/api/health.go",
    "chars": 273,
    "preview": "package api\n\nimport \"net/http\"\n\n// GET /health\nfunc (api *API) Health(w http.ResponseWriter, _ *http.Request) error {\n\ti"
  },
  {
    "path": "pkg/api/http.go",
    "chars": 2098,
    "preview": "package api\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\n\t\"github.com/gobuffalo/packr/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n)"
  },
  {
    "path": "pkg/api/http_test.go",
    "chars": 528,
    "preview": "package api\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n)\n\nfunc TestRespond(t *testing.T) {\n\t"
  },
  {
    "path": "pkg/api/page_stats.go",
    "chars": 773,
    "preview": "package api\n\nimport (\n\t\"net/http\"\n)\n\n// URL: /api/sites/{id:[0-9]+}/stats/pages/agg\nfunc (api *API) GetAggregatedPageSta"
  },
  {
    "path": "pkg/api/params.go",
    "chars": 1339,
    "preview": "package api\n\nimport (\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/gorilla/mux\"\n)\n\n// Params defines the commonly used A"
  },
  {
    "path": "pkg/api/params_test.go",
    "chars": 795,
    "preview": "package api\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestGetRequestParams(t *testing.T) {\n\tstartDate := t"
  },
  {
    "path": "pkg/api/referrer_stats.go",
    "chars": 742,
    "preview": "package api\n\nimport (\n\t\"net/http\"\n)\n\nfunc (api *API) GetAggregatedReferrerStatsHandler(w http.ResponseWriter, r *http.Re"
  },
  {
    "path": "pkg/api/routes.go",
    "chars": 2629,
    "preview": "package api\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/gobuffalo/packr/v2\"\n\t\"github.com/gorilla/mux\"\n)\n\nfunc (api *API) Routes("
  },
  {
    "path": "pkg/api/site_stats.go",
    "chars": 1083,
    "preview": "package api\n\nimport (\n\t\"net/http\"\n)\n\n// URL: /api/sites/{id:[0-9]+}/stats/site/agg\nfunc (api *API) GetAggregatedSiteStat"
  },
  {
    "path": "pkg/api/sites.go",
    "chars": 1699,
    "preview": "package api\n\nimport (\n\t\"encoding/json\"\n\t\"math/rand\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/gorilla/mux\"\n\t\"github.c"
  },
  {
    "path": "pkg/cli/cli.go",
    "chars": 1494,
    "preview": "package cli\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/urfave/cli\"\n\t\"gith"
  },
  {
    "path": "pkg/cli/server.go",
    "chars": 1981,
    "preview": "package cli\n\nimport (\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/urfave/cli\"\n\n\t\"gith"
  },
  {
    "path": "pkg/cli/stats.go",
    "chars": 1628,
    "preview": "package cli\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/urfave/cli\"\n)\n\nvar statsCmd = cli.Co"
  },
  {
    "path": "pkg/cli/user.go",
    "chars": 2210,
    "preview": "package cli\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/usefathom/fathom/pkg/models\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"g"
  },
  {
    "path": "pkg/config/config.go",
    "chars": 2037,
    "preview": "package config\n\nimport (\n\t\"math/rand\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/joho/godotenv\"\n\t\"github.com/kelsey"
  },
  {
    "path": "pkg/config/config_test.go",
    "chars": 1831,
    "preview": "package config\n\nimport (\n\t\"io/ioutil\"\n\t\"os\"\n\t\"testing\"\n)\n\nfunc TestLoadEnv(t *testing.T) {\n\tbefore := len(os.Environ())\n"
  },
  {
    "path": "pkg/datastore/datastore.go",
    "chars": 2161,
    "preview": "package datastore\n\nimport (\n\t\"time\"\n\n\t\"github.com/usefathom/fathom/pkg/datastore/sqlstore\"\n\t\"github.com/usefathom/fathom"
  },
  {
    "path": "pkg/datastore/sqlstore/config.go",
    "chars": 1627,
    "preview": "package sqlstore\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n\n\tmysql \"github.com/go-sql-driver/mysql\"\n)\n\ntype Config struct {\n\tDriver"
  },
  {
    "path": "pkg/datastore/sqlstore/config_test.go",
    "chars": 1125,
    "preview": "package sqlstore\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\nfunc TestConfigDSN(t *testing.T) {\n\tc := Config{\n\t\tDriver:   \"postgres\","
  },
  {
    "path": "pkg/datastore/sqlstore/hostnames.go",
    "chars": 681,
    "preview": "package sqlstore\n\nimport (\n\t\"database/sql\"\n)\n\nfunc (db *sqlstore) HostnameID(name string) (int64, error) {\n\tvar id int64"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/10_alter_stats_table_constraints.sql",
    "chars": 979,
    "preview": "-- +migrate Up\n\nDROP INDEX unique_daily_site_stats ON daily_site_stats; \nDROP INDEX unique_daily_page_stats ON daily_pag"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/11_add_pageview_finished_column.sql",
    "chars": 158,
    "preview": "-- +migrate Up\n\nALTER TABLE pageviews ADD COLUMN is_finished TINYINT(1) NOT NULL DEFAULT 0;\n\n-- +migrate Down\n\nALTER TAB"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/12_create_hostnames_table.sql",
    "chars": 206,
    "preview": "-- +migrate Up\nCREATE TABLE hostnames(\n   id INTEGER AUTO_INCREMENT PRIMARY KEY NOT NULL,\n   name VARCHAR(255) NOT NULL\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/13_create_unique_hostname_index.sql",
    "chars": 144,
    "preview": "-- +migrate Up\nCREATE UNIQUE INDEX unique_hostnames_name ON hostnames(name(100));\n\n-- +migrate Down\nDROP INDEX IF EXISTS"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/14_create_pathnames_table.sql",
    "chars": 206,
    "preview": "-- +migrate Up\nCREATE TABLE pathnames(\n   id INTEGER AUTO_INCREMENT PRIMARY KEY NOT NULL,\n   name VARCHAR(255) NOT NULL\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/15_create_unique_pathname_index.sql",
    "chars": 144,
    "preview": "-- +migrate Up\nCREATE UNIQUE INDEX unique_pathnames_name ON pathnames(name(100));\n\n-- +migrate Down\nDROP INDEX IF EXISTS"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/16_fill_hostnames_table.sql",
    "chars": 149,
    "preview": "-- +migrate Up\nINSERT INTO hostnames(name) SELECT hostname FROM daily_page_stats UNION SELECT hostname FROM daily_referr"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/17_fill_pathnames_table.sql",
    "chars": 149,
    "preview": "-- +migrate Up\nINSERT INTO pathnames(name) SELECT pathname FROM daily_page_stats UNION SELECT pathname FROM daily_referr"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/18_alter_page_stats_table.sql",
    "chars": 820,
    "preview": "-- +migrate Up\nDROP TABLE IF EXISTS daily_page_stats_old;\nRENAME TABLE daily_page_stats TO daily_page_stats_old;\nCREATE "
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/19_alter_referrer_stats_table.sql",
    "chars": 871,
    "preview": "-- +migrate Up\nDROP TABLE IF EXISTS daily_referrer_stats_old;\nRENAME TABLE daily_referrer_stats TO daily_referrer_stats_"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/1_initial_tables.sql",
    "chars": 1918,
    "preview": "-- +migrate Up\n\nCREATE TABLE users (\n  id INT AUTO_INCREMENT PRIMARY KEY NOT NULL,\n  email VARCHAR(100) NOT NULL,\n  pass"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/20_recreate_stats_indices.sql",
    "chars": 372,
    "preview": "-- +migrate Up\nCREATE UNIQUE INDEX unique_daily_page_stats ON daily_page_stats(site_id, hostname_id, pathname_id, date);"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/21_alter_page_stats_table.sql",
    "chars": 637,
    "preview": "-- +migrate Up\nCREATE TABLE page_stats(\n   site_id INTEGER NOT NULL DEFAULT 1,\n   hostname_id INTEGER NOT NULL,\n   pathn"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/22_alter_site_stats_table.sql",
    "chars": 548,
    "preview": "-- +migrate Up\nCREATE TABLE site_stats(\n   site_id INTEGER NOT NULL DEFAULT 1,\n   pageviews INTEGER NOT NULL,\n   visitor"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/23_alter_referrer_stats_table.sql",
    "chars": 690,
    "preview": "-- +migrate Up\nCREATE TABLE referrer_stats(\n    site_id INTEGER NOT NULL DEFAULT 1, \n    hostname_id INTEGER NOT NULL,\n "
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/24_recreate_stat_table_indices.sql",
    "chars": 431,
    "preview": "-- +migrate Up\nCREATE UNIQUE INDEX unique_page_stats ON page_stats(site_id, hostname_id, pathname_id, ts);\nCREATE UNIQUE"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/2_known_durations_column.sql",
    "chars": 471,
    "preview": "-- +migrate Up\n\nALTER TABLE daily_site_stats ADD COLUMN known_durations INTEGER NOT NULL DEFAULT 0;\nALTER TABLE daily_pa"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/3_referrer_group_column.sql",
    "chars": 961,
    "preview": "-- +migrate Up\n\nDROP INDEX unique_daily_referrer_stats ON daily_referrer_stats;\n\nALTER TABLE daily_referrer_stats ADD CO"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/4_pageview_id_column.sql",
    "chars": 382,
    "preview": "-- +migrate Up\n\nALTER TABLE pageviews DROP COLUMN session_id;\nALTER TABLE pageviews DROP COLUMN id;\nALTER TABLE pageview"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/5_create_sites_table.sql",
    "chars": 235,
    "preview": "-- +migrate Up\nCREATE TABLE sites (\n    id INTEGER AUTO_INCREMENT PRIMARY KEY NOT NULL,\n    tracking_id VARCHAR(8) UNIQU"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/6_add_site_tracking_id_column_to_pageviews_table.sql",
    "chars": 179,
    "preview": "-- +migrate Up\n\nTRUNCATE pageviews;\nALTER TABLE pageviews ADD COLUMN site_tracking_id VARCHAR(8) NOT NULL;\n\n-- +migrate "
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/7_add_site_id_to_site_stats_table.sql",
    "chars": 162,
    "preview": "-- +migrate Up\n\nALTER TABLE daily_site_stats ADD COLUMN site_id INTEGER NOT NULL DEFAULT 1;\n\n-- +migrate Down\n\nALTER TAB"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/8_add_site_id_to_page_stats_table.sql",
    "chars": 162,
    "preview": "-- +migrate Up\n\nALTER TABLE daily_page_stats ADD COLUMN site_id INTEGER NOT NULL DEFAULT 1;\n\n-- +migrate Down\n\nALTER TAB"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/9_add_site_id_to_referrer_stats_table.sql",
    "chars": 170,
    "preview": "-- +migrate Up\n\nALTER TABLE daily_referrer_stats ADD COLUMN site_id INTEGER NOT NULL DEFAULT 1;\n\n-- +migrate Down\n\nALTER"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/10_alter_numeric_column_precision.sql",
    "chars": 455,
    "preview": "-- +migrate Up\n\nALTER TABLE daily_site_stats ALTER COLUMN bounce_rate TYPE NUMERIC;\nALTER TABLE daily_page_stats ALTER C"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/11_alter_stats_table_constraints.sql",
    "chars": 869,
    "preview": "-- +migrate Up\n\nDROP INDEX IF EXISTS unique_daily_site_stats;\nDROP INDEX IF EXISTS unique_daily_page_stats;\nDROP INDEX I"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/12_add_pageview_finished_column.sql",
    "chars": 159,
    "preview": "-- +migrate Up\n\nALTER TABLE pageviews ADD COLUMN is_finished BOOLEAN NOT NULL DEFAULT FALSE;\n\n-- +migrate Down\n\nALTER TA"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/13_create_hostnames_table.sql",
    "chars": 157,
    "preview": "-- +migrate Up\nCREATE TABLE hostnames(\n   id SERIAL PRIMARY KEY NOT NULL,\n   name VARCHAR(255) NOT NULL\n);\n\n-- +migrate "
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/14_create_unique_hostname_index.sql",
    "chars": 139,
    "preview": "-- +migrate Up\nCREATE UNIQUE INDEX unique_hostnames_name ON hostnames(name);\n\n-- +migrate Down\nDROP INDEX IF EXISTS uniq"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/15_create_pathnames_table.sql",
    "chars": 157,
    "preview": "-- +migrate Up\nCREATE TABLE pathnames(\n   id SERIAL PRIMARY KEY NOT NULL,\n   name VARCHAR(255) NOT NULL\n);\n\n-- +migrate "
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/16_create_unique_pathname_index.sql",
    "chars": 139,
    "preview": "-- +migrate Up\nCREATE UNIQUE INDEX unique_pathnames_name ON pathnames(name);\n\n-- +migrate Down\nDROP INDEX IF EXISTS uniq"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/17_fill_hostnames_table.sql",
    "chars": 149,
    "preview": "-- +migrate Up\nINSERT INTO hostnames(name) SELECT hostname FROM daily_page_stats UNION SELECT hostname FROM daily_referr"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/18_fill_pathnames_table.sql",
    "chars": 149,
    "preview": "-- +migrate Up\nINSERT INTO pathnames(name) SELECT pathname FROM daily_page_stats UNION SELECT pathname FROM daily_referr"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/19_alter_page_stats_table.sql",
    "chars": 807,
    "preview": "-- +migrate Up\nDROP TABLE IF EXISTS daily_page_stats_old;\nALTER TABLE daily_page_stats RENAME TO daily_page_stats_old;\nC"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/1_initial_tables.sql",
    "chars": 1779,
    "preview": "-- +migrate Up\n\nCREATE TABLE users(\n  id SERIAL PRIMARY KEY NOT NULL,\n  email VARCHAR(255) NOT NULL,\n  password VARCHAR("
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/20_alter_referrer_stats_table.sql",
    "chars": 858,
    "preview": "-- +migrate Up\nDROP TABLE IF EXISTS daily_referrer_stats_old;\nALTER TABLE daily_referrer_stats RENAME TO daily_referrer_"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/21_recreate_stats_indices.sql",
    "chars": 328,
    "preview": "-- +migrate Up\nCREATE UNIQUE INDEX unique_daily_page_stats ON daily_page_stats(site_id, hostname_id, pathname_id, date);"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/22_alter_page_stats_table.sql",
    "chars": 645,
    "preview": "-- +migrate Up\nCREATE TABLE page_stats(\n   site_id INTEGER NOT NULL DEFAULT 1,\n   hostname_id INTEGER NOT NULL,\n   pathn"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/23_alter_referrer_stats_table.sql",
    "chars": 683,
    "preview": "-- +migrate Up\nCREATE TABLE referrer_stats(\n    site_id INTEGER NOT NULL DEFAULT 1, \n    hostname_id INTEGER NOT NULL,\n "
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/24_alter_site_stats_table.sql",
    "chars": 553,
    "preview": "-- +migrate Up\nCREATE TABLE site_stats(\n   site_id INTEGER NOT NULL DEFAULT 1,\n   pageviews INTEGER NOT NULL,\n   visitor"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/25_recreate_stat_table_indices.sql",
    "chars": 510,
    "preview": "-- +migrate Up\nDROP INDEX IF EXISTS unique_daily_page_stats;\nDROP INDEX IF EXISTS unique_daily_referrer_stats;\nCREATE UN"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/26_alter_pageviews_table.sql",
    "chars": 189,
    "preview": "-- +migrate Up\n\nALTER TABLE pageviews ALTER COLUMN timestamp TYPE TIMESTAMP WITHOUT TIME ZONE;\n\n-- +migrate Down\n\nALTER "
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/2_known_durations_column.sql",
    "chars": 471,
    "preview": "-- +migrate Up\n\nALTER TABLE daily_site_stats ADD COLUMN known_durations INTEGER NOT NULL DEFAULT 0;\nALTER TABLE daily_pa"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/3_referrer_group_column.sql",
    "chars": 998,
    "preview": "-- +migrate Up\n\nALTER TABLE daily_referrer_stats ADD COLUMN groupname VARCHAR(255);\nALTER TABLE daily_referrer_stats ADD"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/4_pageview_id_column.sql",
    "chars": 436,
    "preview": "-- +migrate Up\n\nTRUNCATE pageviews; -- postgres will fail because of NULL values otherwise\nALTER TABLE pageviews DROP CO"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/5_create_sites_table.sql",
    "chars": 186,
    "preview": "-- +migrate Up\nCREATE TABLE sites (\n    id SERIAL PRIMARY KEY NOT NULL,\n    tracking_id VARCHAR(8) UNIQUE,\n    name VARC"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/6_add_site_tracking_id_column_to_pageviews_table.sql",
    "chars": 234,
    "preview": "-- +migrate Up\n\nTRUNCATE pageviews; -- postgres will fail because of NULL values otherwise\nALTER TABLE pageviews ADD COL"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/7_add_site_id_to_site_stats_table.sql",
    "chars": 162,
    "preview": "-- +migrate Up\n\nALTER TABLE daily_site_stats ADD COLUMN site_id INTEGER NOT NULL DEFAULT 1;\n\n-- +migrate Down\n\nALTER TAB"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/8_add_site_id_to_page_stats_table.sql",
    "chars": 162,
    "preview": "-- +migrate Up\n\nALTER TABLE daily_page_stats ADD COLUMN site_id INTEGER NOT NULL DEFAULT 1;\n\n-- +migrate Down\n\nALTER TAB"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/9_add_site_id_to_referrer_stats_table.sql",
    "chars": 170,
    "preview": "-- +migrate Up\n\nALTER TABLE daily_referrer_stats ADD COLUMN site_id INTEGER NOT NULL DEFAULT 1;\n\n-- +migrate Down\n\nALTER"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/10_alter_stats_table_constraints.sql",
    "chars": 869,
    "preview": "-- +migrate Up\n\nDROP INDEX IF EXISTS unique_daily_site_stats;\nDROP INDEX IF EXISTS unique_daily_page_stats;\nDROP INDEX I"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/11_add_pageview_finished_column.sql",
    "chars": 942,
    "preview": "-- +migrate Up\n\nDROP TABLE IF EXISTS pageviews;\nCREATE TABLE pageviews(\n   id VARCHAR(31) NOT NULL,\n   site_tracking_id "
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/12_create_hostnames_table.sql",
    "chars": 149,
    "preview": "-- +migrate Up\nCREATE TABLE hostnames(\n   id INTEGER PRIMARY KEY,\n   name VARCHAR(255) NOT NULL\n);\n\n-- +migrate Down\nDRO"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/13_create_unique_hostname_index.sql",
    "chars": 139,
    "preview": "-- +migrate Up\nCREATE UNIQUE INDEX unique_hostnames_name ON hostnames(name);\n\n-- +migrate Down\nDROP INDEX IF EXISTS uniq"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/14_create_pathnames_table.sql",
    "chars": 149,
    "preview": "-- +migrate Up\nCREATE TABLE pathnames(\n   id INTEGER PRIMARY KEY,\n   name VARCHAR(255) NOT NULL\n);\n\n-- +migrate Down\nDRO"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/15_create_unique_pathname_index.sql",
    "chars": 139,
    "preview": "-- +migrate Up\nCREATE UNIQUE INDEX unique_pathnames_name ON pathnames(name);\n\n-- +migrate Down\nDROP INDEX IF EXISTS uniq"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/15_vacuum.sql",
    "chars": 56,
    "preview": "-- +migrate Up  notransaction\nVACUUM;\n\n-- +migrate Down "
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/16_fill_hostnames_table.sql",
    "chars": 150,
    "preview": "-- +migrate Up \nINSERT INTO hostnames(name) SELECT hostname FROM daily_page_stats UNION SELECT hostname FROM daily_refer"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/17_fill_pathnames_table.sql",
    "chars": 149,
    "preview": "-- +migrate Up\nINSERT INTO pathnames(name) SELECT pathname FROM daily_page_stats UNION SELECT pathname FROM daily_referr"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/18_alter_page_stats_table.sql",
    "chars": 807,
    "preview": "-- +migrate Up\nDROP TABLE IF EXISTS daily_page_stats_old;\nALTER TABLE daily_page_stats RENAME TO daily_page_stats_old;\nC"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/19_alter_referrer_stats_table.sql",
    "chars": 858,
    "preview": "-- +migrate Up\nDROP TABLE IF EXISTS daily_referrer_stats_old;\nALTER TABLE daily_referrer_stats RENAME TO daily_referrer_"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/1_initial_tables.sql",
    "chars": 1733,
    "preview": "-- +migrate Up\n\nCREATE TABLE users (\n  id INTEGER PRIMARY KEY,\n  email VARCHAR(255) NOT NULL,\n  password VARCHAR(255) NO"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/20_recreate_stats_indices.sql",
    "chars": 444,
    "preview": "-- +migrate Up\nDROP INDEX IF EXISTS unique_daily_page_stats;\nDROP INDEX IF EXISTS unique_daily_referrer_stats;\nCREATE UN"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/21_alter_page_stats_table.sql",
    "chars": 612,
    "preview": "-- +migrate Up\nCREATE TABLE page_stats(\n   site_id INTEGER NOT NULL DEFAULT 1,\n   hostname_id INTEGER NOT NULL,\n   pathn"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/22_alter_site_stats_table.sql",
    "chars": 522,
    "preview": "-- +migrate Up\nCREATE TABLE site_stats(\n   site_id INTEGER NOT NULL DEFAULT 1,\n   pageviews INTEGER NOT NULL,\n   visitor"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/23_alter_referrer_stats_table.sql",
    "chars": 652,
    "preview": "-- +migrate Up\nCREATE TABLE referrer_stats(\n    site_id INTEGER NOT NULL DEFAULT 1, \n    hostname_id INTEGER NOT NULL,\n "
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/24_recreate_stat_table_indices.sql",
    "chars": 510,
    "preview": "-- +migrate Up\nDROP INDEX IF EXISTS unique_daily_page_stats;\nDROP INDEX IF EXISTS unique_daily_referrer_stats;\nCREATE UN"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/25_vacuum.sql",
    "chars": 56,
    "preview": "-- +migrate Up  notransaction\nVACUUM;\n\n-- +migrate Down "
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/26_sites_id_autoinc.sql",
    "chars": 575,
    "preview": "-- +migrate Up\nDROP TABLE IF EXISTS sites_old;\nALTER TABLE sites RENAME TO sites_old;\nCREATE TABLE sites (\n    `id` INTE"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/2_known_durations_column.sql",
    "chars": 471,
    "preview": "-- +migrate Up\n\nALTER TABLE daily_site_stats ADD COLUMN known_durations INTEGER NOT NULL DEFAULT 0;\nALTER TABLE daily_pa"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/3_referrer_group_column.sql",
    "chars": 1176,
    "preview": "-- +migrate Up\n\nALTER TABLE daily_referrer_stats ADD COLUMN groupname VARCHAR(255);\nALTER TABLE daily_referrer_stats ADD"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/4_pageview_id_column.sql",
    "chars": 830,
    "preview": "-- +migrate Up\n\nDROP TABLE pageviews;\nCREATE TABLE pageviews(\n   id VARCHAR(31) NOT NULL,\n   hostname VARCHAR(255) NOT N"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/5_create_sites_table.sql",
    "chars": 178,
    "preview": "-- +migrate Up\nCREATE TABLE sites (\n    id INTEGER PRIMARY KEY,\n    tracking_id VARCHAR(8) UNIQUE,\n    name VARCHAR(100)"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/6_add_site_tracking_id_column_to_pageviews_table.sql",
    "chars": 858,
    "preview": "-- +migrate Up\n\nDROP TABLE IF EXISTS pageviews;\nCREATE TABLE pageviews(\n   id VARCHAR(31) NOT NULL,\n   site_tracking_id "
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/7_add_site_id_to_site_stats_table.sql",
    "chars": 112,
    "preview": "-- +migrate Up\n\nALTER TABLE daily_site_stats ADD COLUMN site_id INTEGER NOT NULL DEFAULT 1;\n\n-- +migrate Down\n\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/8_add_site_id_to_page_stats_table.sql",
    "chars": 112,
    "preview": "-- +migrate Up\n\nALTER TABLE daily_page_stats ADD COLUMN site_id INTEGER NOT NULL DEFAULT 1;\n\n-- +migrate Down\n\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/9_add_site_id_to_referrer_stats_table.sql",
    "chars": 116,
    "preview": "-- +migrate Up\n\nALTER TABLE daily_referrer_stats ADD COLUMN site_id INTEGER NOT NULL DEFAULT 1;\n\n-- +migrate Down\n\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/page_stats.go",
    "chars": 2902,
    "preview": "package sqlstore\n\nimport (\n\t\"database/sql\"\n\t\"time\"\n\n\t\"github.com/usefathom/fathom/pkg/models\"\n)\n\nfunc (db *sqlstore) Get"
  },
  {
    "path": "pkg/datastore/sqlstore/pageviews.go",
    "chars": 3657,
    "preview": "package sqlstore\n\nimport (\n\t\"strings\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/usefathom/fathom/pkg/model"
  },
  {
    "path": "pkg/datastore/sqlstore/pathnames.go",
    "chars": 681,
    "preview": "package sqlstore\n\nimport (\n\t\"database/sql\"\n)\n\nfunc (db *sqlstore) PathnameID(name string) (int64, error) {\n\tvar id int64"
  },
  {
    "path": "pkg/datastore/sqlstore/referrer_stats.go",
    "chars": 3215,
    "preview": "package sqlstore\n\nimport (\n\t\"database/sql\"\n\t\"time\"\n\n\t\"github.com/usefathom/fathom/pkg/models\"\n)\n\nfunc (db *sqlstore) Get"
  },
  {
    "path": "pkg/datastore/sqlstore/seed/pageviews.sql",
    "chars": 3526,
    "preview": "INSERT INTO pageviews \n   ( session_id, pathname, is_new_visitor, is_unique, is_bounce, referrer, duration, timestamp) V"
  },
  {
    "path": "pkg/datastore/sqlstore/site_stats.go",
    "chars": 3337,
    "preview": "package sqlstore\n\nimport (\n\t\"database/sql\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/usefathom/fathom/pkg/"
  },
  {
    "path": "pkg/datastore/sqlstore/sites.go",
    "chars": 1814,
    "preview": "package sqlstore\n\nimport (\n\t\"database/sql\"\n\n\t\"github.com/usefathom/fathom/pkg/models\"\n)\n\n// GetSites gets all sites in t"
  },
  {
    "path": "pkg/datastore/sqlstore/sqlstore.go",
    "chars": 2178,
    "preview": "package sqlstore\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"time\"\n\n\t_ \"github.com/go-sql-driver/mysql\" // mysql dr"
  },
  {
    "path": "pkg/datastore/sqlstore/users.go",
    "chars": 2160,
    "preview": "package sqlstore\n\nimport (\n\t\"database/sql\"\n\n\t\"github.com/usefathom/fathom/pkg/models\"\n)\n\n// GetUser retrieves user from "
  },
  {
    "path": "pkg/models/page_stats.go",
    "chars": 1189,
    "preview": "package models\n\nimport (\n\t\"time\"\n)\n\ntype PageStats struct {\n\tNew            bool      `db:\"-\" json:\"-\"`\n\tSiteID         "
  },
  {
    "path": "pkg/models/page_stats_test.go",
    "chars": 1609,
    "preview": "package models\n\nimport \"testing\"\n\nfunc TestPageStatsHandlePageview(t *testing.T) {\n\ts := PageStats{}\n\n\tp1 := &Pageview{\n"
  },
  {
    "path": "pkg/models/pageview.go",
    "chars": 585,
    "preview": "package models\n\nimport (\n\t\"time\"\n)\n\ntype Pageview struct {\n\tID             string    `db:\"id\"`\n\tSiteTrackingID string   "
  },
  {
    "path": "pkg/models/referrer_stats.go",
    "chars": 1163,
    "preview": "package models\n\nimport (\n\t\"time\"\n)\n\ntype ReferrerStats struct {\n\tNew            bool      `db:\"-\" json:\"-\"`\n\tSiteID     "
  },
  {
    "path": "pkg/models/referrer_stats_test.go",
    "chars": 1356,
    "preview": "package models\n\nimport \"testing\"\n\nfunc TestReferrerStatsHandlePageview(t *testing.T) {\n\ts := ReferrerStats{}\n\tp1 := &Pag"
  },
  {
    "path": "pkg/models/site.go",
    "chars": 220,
    "preview": "package models\n\n// Site represents a group for tracking data\ntype Site struct {\n\tID         int64  `db:\"id\" json:\"id\"`\n\t"
  },
  {
    "path": "pkg/models/site_stats.go",
    "chars": 1160,
    "preview": "package models\n\nimport (\n\t\"fmt\"\n\t\"time\"\n)\n\ntype SiteStats struct {\n\tNew            bool      `db:\"-\" json:\"-\" `\n\tSiteID "
  },
  {
    "path": "pkg/models/site_stats_test.go",
    "chars": 1862,
    "preview": "package models\n\nimport (\n\t\"testing\"\n)\n\nfunc TestSiteStatsFormattedDuration(t *testing.T) {\n\ts := SiteStats{\n\t\tAvgDuratio"
  },
  {
    "path": "pkg/models/user.go",
    "chars": 793,
    "preview": "package models\n\nimport (\n\t\"strings\"\n\n\t\"golang.org/x/crypto/bcrypt\"\n)\n\ntype User struct {\n\tID       int64\n\tEmail    strin"
  },
  {
    "path": "pkg/models/user_test.go",
    "chars": 487,
    "preview": "package models\n\nimport (\n\t\"testing\"\n)\n\nfunc TestNewUser(t *testing.T) {\n\temail := \"foo@bar.com\"\n\tpwd := \"passw0rd01\"\n\tu "
  }
]

About this extraction

This page contains the full source code of the usefathom/fathom GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 181 files (342.0 KB), approximately 131.7k tokens, and a symbol index with 409 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!