├── .npmrc ├── src ├── routes │ ├── +layout.ts │ ├── +layout.svelte │ └── +page.svelte ├── app.d.ts ├── lib │ ├── types │ │ ├── api.ts │ │ └── config.ts │ ├── components │ │ ├── Footer.svelte │ │ ├── Header.svelte │ │ ├── StatusGroup.svelte │ │ ├── OverallStatus.svelte │ │ ├── RefreshSettings.svelte │ │ ├── Status.svelte │ │ ├── Announcements.svelte │ │ └── Loader.svelte │ └── persistent_store.ts ├── app.html └── app.css ├── docs ├── demo-dark.png ├── demo-light.png └── example-nginx.conf ├── static ├── favicon.ico └── img │ └── logo.png ├── .prettierignore ├── .devcontainer ├── docker-compose.yml └── devcontainer.json ├── vite.config.ts ├── .gitignore ├── .github ├── dependabot.yml ├── changelog-configuration.json └── workflows │ ├── dependabot-auto-merge.yml │ └── release.yml ├── .prettierrc ├── svelte.config.js ├── tsconfig.json ├── NOTICE.md ├── eslint.config.js ├── package.json ├── README.md └── LICENSE /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | export const ssr = false 2 | -------------------------------------------------------------------------------- /docs/demo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BluemediaDev/fancy-gatus/HEAD/docs/demo-dark.png -------------------------------------------------------------------------------- /docs/demo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BluemediaDev/fancy-gatus/HEAD/docs/demo-light.png -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BluemediaDev/fancy-gatus/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BluemediaDev/fancy-gatus/HEAD/static/img/logo.png -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Package Managers 2 | package-lock.json 3 | pnpm-lock.yaml 4 | yarn.lock 5 | bun.lock 6 | bun.lockb 7 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | image: mcr.microsoft.com/devcontainers/javascript-node:1-22-bookworm 4 | volumes: 5 | - ..:/fancy-gatus:cached 6 | command: sleep infinity 7 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite' 2 | import tailwindcss from '@tailwindcss/vite' 3 | import { defineConfig } from 'vite' 4 | 5 | export default defineConfig({ 6 | plugins: [tailwindcss(), sveltekit()], 7 | server: { 8 | host: '127.0.0.1', 9 | }, 10 | }) 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Output 4 | .output 5 | .vercel 6 | .netlify 7 | .wrangler 8 | /.svelte-kit 9 | /build 10 | 11 | # OS 12 | .DS_Store 13 | Thumbs.db 14 | 15 | # Env 16 | .env 17 | .env.* 18 | !.env.example 19 | !.env.test 20 | 21 | # Vite 22 | vite.config.js.timestamp-* 23 | vite.config.ts.timestamp-* 24 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://svelte.dev/docs/kit/types#app.d.ts 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | } 12 | 13 | export {} 14 | -------------------------------------------------------------------------------- /src/lib/types/api.ts: -------------------------------------------------------------------------------- 1 | export type Result = { 2 | status?: number 3 | hostname?: string 4 | duration: number 5 | conditionResults: { 6 | condition: string 7 | success: boolean 8 | }[] 9 | success: boolean 10 | timestamp: string 11 | } 12 | 13 | export type Status = { 14 | name: string 15 | group?: string 16 | key: string 17 | results: Result[] 18 | } 19 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: 'npm' 7 | directory: '/' 8 | schedule: 9 | interval: 'weekly' 10 | labels: 11 | - 'dependencies' 12 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "semi": false, 4 | "singleQuote": true, 5 | "useTabs": false, 6 | "tabWidth": 2, 7 | "quoteProps": "consistent", 8 | "bracketSpacing": true, 9 | "arrowParens": "always", 10 | "printWidth": 100, 11 | "plugins": ["prettier-plugin-svelte"], 12 | "overrides": [ 13 | { 14 | "files": "*.svelte", 15 | "options": { 16 | "parser": "svelte" 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/components/Footer.svelte: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Infrastructure Status 6 | 7 | 8 | %sveltekit.head% 9 | 10 | 11 |
%sveltekit.body%
12 | 13 | 14 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
11 |
12 | {@render children()} 13 |
14 |
15 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-static' 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://svelte.dev/docs/kit/integrations 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | kit: { 11 | adapter: adapter({ 12 | fallback: 'index.html', 13 | }), 14 | }, 15 | } 16 | 17 | export default config 18 | -------------------------------------------------------------------------------- /.github/changelog-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "categories": [ 3 | { 4 | "title": "## 🚀 Features", 5 | "labels": ["enhancement"] 6 | }, 7 | { 8 | "title": "## 🐛 Fixes", 9 | "labels": ["bug"] 10 | }, 11 | { 12 | "title": "## ⤵️ Dependencies", 13 | "labels": ["dependencies"] 14 | }, 15 | { 16 | "title": "## 💭 Uncategorized", 17 | "labels": [] 18 | } 19 | ], 20 | "template": "#{{CHANGELOG}}", 21 | "pr_template": "* #{{TITLE}} by @#{{AUTHOR}} in ##{{NUMBER}}" 22 | } 23 | -------------------------------------------------------------------------------- /src/lib/persistent_store.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store' 2 | 3 | interface PersistedSettings { 4 | darkmode: boolean 5 | } 6 | 7 | const settingsDefault: PersistedSettings = { 8 | darkmode: window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches, 9 | } 10 | 11 | export const persistentSettings = writable( 12 | JSON.parse(localStorage.getItem('persistentSettings') || JSON.stringify(settingsDefault)) 13 | ) 14 | 15 | persistentSettings.subscribe((value) => (localStorage.persistentSettings = JSON.stringify(value))) 16 | -------------------------------------------------------------------------------- /src/lib/types/config.ts: -------------------------------------------------------------------------------- 1 | export type Announcement = { 2 | type: 'outage' | 'warning' | 'information' | 'operational' | 'none' 3 | message: string 4 | timestamp: string 5 | } 6 | 7 | export type GatusConfig = { 8 | oidc: boolean 9 | authenticated: boolean 10 | announcements: Announcement[] 11 | } 12 | 13 | export type FrontendConfig = { 14 | title?: string 15 | gatusBaseUrl?: string 16 | hiddenGroups?: string[] 17 | hiddenStatuses?: string[] 18 | groupOrder?: string[] 19 | defaultExpandGroups?: boolean 20 | defaultRefreshInterval?: 10 | 30 | 60 | 120 | 300 | 600 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias 15 | // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files 16 | // 17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 18 | // from the referenced tsconfig.json - TypeScript does not merge them in 19 | } 20 | -------------------------------------------------------------------------------- /docs/example-nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 443 ssl http2; 3 | listen [::]:443 ssl http2; 4 | 5 | server_name status.example.com; 6 | 7 | access_log /var/log/nginx/access.log; 8 | error_log /var/log/nginx/error.log warn; 9 | 10 | ssl_certificate /path/to/cert.pem; 11 | ssl_certificate_key /path/to/cert.key; 12 | 13 | # Make Gatus API available relative to the frontend 14 | location ~ "/api/v1/(config|endpoints/statuses)" { 15 | proxy_pass http://gatus:8080; 16 | proxy_set_header Host $host; 17 | proxy_redirect http:// https://; 18 | proxy_http_version 1.1; 19 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 20 | } 21 | 22 | # Frontend 23 | root /var/www/html; 24 | index index.html index.htm; 25 | location / { 26 | try_files $uri $uri/ =404; 27 | } 28 | } -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node 3 | { 4 | "name": "Node.js", 5 | 6 | "dockerComposeFile": "docker-compose.yml", 7 | "service": "app", 8 | "workspaceFolder": "/fancy-gatus", 9 | "shutdownAction": "stopCompose", 10 | 11 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 12 | "forwardPorts": [5173], 13 | 14 | // Configure tool-specific properties. 15 | "customizations": { 16 | "vscode": { 17 | "extensions": [ 18 | "svelte.svelte-vscode", 19 | "dbaeumer.vscode-eslint", 20 | "esbenp.prettier-vscode", 21 | "eamodio.gitlens" 22 | ] 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /NOTICE.md: -------------------------------------------------------------------------------- 1 | # Notices 2 | 3 | This file contains notices for some releases. If this file contains a section for a given release, it will be automatically included in the release notes above the generated changelog. 4 | 5 | ## v3.0.0 6 | 7 | This release contains breaking changes and requires Gatus `>= v5.23.0`. 8 | 9 | ### New announcement feature 10 | 11 | Fancy Gatus now uses the new announcement feature included in Gatus. Therefore the `notice` section in the frontend config is no longer used. Please move your announcements/notices to the Gatus config before upgrading. This change also requires that the frontend can access the `/api/v1/config` endpoint of Gatus. Please adjust your proxy config accordingly; an example configuration can be found [here](https://github.com/BluemediaDev/fancy-gatus/blob/v3.0.0/docs/example-nginx.conf). 12 | -------------------------------------------------------------------------------- /src/lib/components/Header.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 | Logo 9 |

{title}

10 | 11 | 21 |
22 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: pull_request 3 | 4 | permissions: 5 | contents: write 6 | pull-requests: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'BluemediaDev/fancy-gatus' 12 | steps: 13 | - name: Dependabot metadata 14 | id: metadata 15 | uses: dependabot/fetch-metadata@v2 16 | with: 17 | github-token: '${{ secrets.GITHUB_TOKEN }}' 18 | - name: Enable auto-merge for Dependabot PRs 19 | if: steps.metadata.outputs.update-type == 'version-update:semver-patch' 20 | run: gh pr merge --auto --merge "$PR_URL" 21 | env: 22 | PR_URL: ${{github.event.pull_request.html_url}} 23 | GH_TOKEN: ${{secrets.GITHUB_TOKEN}} 24 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import prettier from 'eslint-config-prettier' 2 | import js from '@eslint/js' 3 | import { includeIgnoreFile } from '@eslint/compat' 4 | import svelte from 'eslint-plugin-svelte' 5 | import globals from 'globals' 6 | import { fileURLToPath } from 'node:url' 7 | import ts from 'typescript-eslint' 8 | import svelteConfig from './svelte.config.js' 9 | 10 | const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)) 11 | 12 | export default ts.config( 13 | includeIgnoreFile(gitignorePath), 14 | js.configs.recommended, 15 | ...ts.configs.recommended, 16 | ...svelte.configs.recommended, 17 | prettier, 18 | ...svelte.configs.prettier, 19 | { 20 | languageOptions: { 21 | globals: { ...globals.browser, ...globals.node }, 22 | }, 23 | rules: { 'no-undef': 'off' }, 24 | }, 25 | { 26 | files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], 27 | languageOptions: { 28 | parserOptions: { 29 | projectService: true, 30 | extraFileExtensions: ['.svelte'], 31 | parser: ts.parser, 32 | svelteConfig, 33 | }, 34 | }, 35 | } 36 | ) 37 | -------------------------------------------------------------------------------- /src/lib/components/StatusGroup.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 |
24 | 25 |
26 | {props.title} 27 |
28 |
29 |
    30 | {#each props.statuses as status (status.key)} 31 | 32 | {/each} 33 |
34 |
35 |
36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fancy-gatus", 3 | "private": true, 4 | "version": "3.0.1", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite dev", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "prepare": "svelte-kit sync || echo ''", 11 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 12 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 13 | "format": "prettier --write .", 14 | "lint": "prettier --check . && eslint ." 15 | }, 16 | "devDependencies": { 17 | "@eslint/compat": "^1.3.2", 18 | "@eslint/js": "^9.38.0", 19 | "@sveltejs/adapter-static": "^3.0.10", 20 | "@sveltejs/kit": "^2.48.0", 21 | "@sveltejs/vite-plugin-svelte": "^6.2.1", 22 | "eslint": "^9.38.0", 23 | "eslint-config-prettier": "^10.1.8", 24 | "eslint-plugin-svelte": "^3.12.5", 25 | "globals": "^16.3.0", 26 | "prettier": "^3.6.2", 27 | "prettier-plugin-svelte": "^3.4.1", 28 | "svelte": "^5.38.3", 29 | "svelte-check": "^4.3.1", 30 | "typescript": "^5.0.0", 31 | "typescript-eslint": "^8.40.0", 32 | "vite": "^7.1.6" 33 | }, 34 | "dependencies": { 35 | "@tailwindcss/vite": "^4.1.11", 36 | "axios": "^1.12.2", 37 | "bootstrap-icons": "^1.13.1", 38 | "daisyui": "^5.0.54", 39 | "tailwindcss": "^4.1.13" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/lib/components/OverallStatus.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 |
24 |
25 |
26 |
34 |
41 |
42 |

{overallStatusText}

43 |
44 |
45 | -------------------------------------------------------------------------------- /src/lib/components/RefreshSettings.svelte: -------------------------------------------------------------------------------- 1 | 30 | 31 | 48 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | @import 'bootstrap-icons'; 3 | 4 | @plugin "daisyui" { 5 | themes: emerald --default; 6 | } 7 | 8 | @plugin "daisyui/theme" { 9 | name: 'emerald'; 10 | --radius-selector: 0.5rem; 11 | --radius-field: 0.25rem; 12 | --radius-box: 0.5rem; 13 | --size-selector: 0.25rem; 14 | --size-field: 0.25rem; 15 | } 16 | 17 | @plugin "daisyui/theme" { 18 | name: 'darkgray'; 19 | default: false; 20 | prefersdark: true; 21 | color-scheme: 'dark'; 22 | --color-base-100: oklch(37% 0.034 259.733); 23 | --color-base-200: oklch(27% 0.033 256.848); 24 | --color-base-300: oklch(21% 0.006 285.885); 25 | --color-base-content: oklch(96% 0.001 286.375); 26 | --color-primary: oklch(70% 0.14 182.503); 27 | --color-primary-content: oklch(98% 0.014 180.72); 28 | --color-secondary: oklch(65% 0.241 354.308); 29 | --color-secondary-content: oklch(94% 0.028 342.258); 30 | --color-accent: oklch(58% 0.233 277.117); 31 | --color-accent-content: oklch(96% 0.018 272.314); 32 | --color-neutral: oklch(20% 0 0); 33 | --color-neutral-content: oklch(96% 0.001 286.375); 34 | --color-info: oklch(74% 0.16 232.661); 35 | --color-info-content: oklch(95% 0.026 236.824); 36 | --color-success: oklch(76% 0.177 163.223); 37 | --color-success-content: oklch(26% 0.051 172.552); 38 | --color-warning: oklch(82% 0.189 84.429); 39 | --color-warning-content: oklch(27% 0.077 45.635); 40 | --color-error: oklch(64% 0.246 16.439); 41 | --color-error-content: oklch(96% 0.015 12.422); 42 | --radius-selector: 0.5rem; 43 | --radius-field: 0.25rem; 44 | --radius-box: 0.5rem; 45 | --size-selector: 0.25rem; 46 | --size-field: 0.25rem; 47 | --border: 1px; 48 | --depth: 0; 49 | --noise: 0; 50 | } 51 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish release 2 | on: 3 | push: 4 | tags: 5 | - 'v*.*.*' 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | release: 12 | name: Release pushed tag 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | ref: ${{ github.ref_name }} 18 | - name: Setup Node.js 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: 22.x 22 | - name: Clean install dependencies and build 23 | run: | 24 | npm ci 25 | npm run build 26 | - name: Create output zip 27 | run: cd build; zip -r ../fancy-gatus-${{ github.ref_name }}.zip * 28 | - name: Build changelog 29 | id: changelog 30 | uses: mikepenz/release-changelog-builder-action@v5 31 | with: 32 | configuration: '.github/changelog-configuration.json' 33 | failOnError: true 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | - name: Get notice from NOTICE.md 37 | id: notice 38 | run: | 39 | echo "content<> $GITHUB_OUTPUT 40 | awk -v ver="${{ github.ref_name }}" '/^## / { if (p) { exit }; if ($2 == ver) { p=1; next } } p' NOTICE.md >> $GITHUB_OUTPUT 41 | echo "EOF" >> $GITHUB_OUTPUT 42 | - name: Publish release 43 | uses: softprops/action-gh-release@v2 44 | with: 45 | name: Fancy Gatus ${{ github.ref_name }} 46 | body: | 47 | ${{ steps.notice.outputs.content }} 48 | ${{ steps.changelog.outputs.changelog }} 49 | files: fancy-gatus-${{ github.ref_name }}.zip 50 | fail_on_unmatched_files: true 51 | -------------------------------------------------------------------------------- /src/lib/components/Status.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |
  • 15 |

    16 | {props.status.name} 17 | {#if props.status.results[props.status.results.length - 1].hostname} 18 | | {props.status.results[props.status.results.length - 1].hostname} 19 | {/if} 20 |

    21 | 22 |
    23 | {#each limitedResults as result (result.timestamp)} 24 |
    25 | 33 |
    36 |
    37 | {/each} 38 |
    39 |
    40 |
    41 |
    49 |
    50 |
    51 | {#if isFailed} 52 |

    Service Problem

    53 | {:else} 54 |

    Operational

    55 | {/if} 56 |
    57 |
  • 58 | -------------------------------------------------------------------------------- /src/lib/components/Announcements.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 | 28 |
    29 | 30 |
    31 | 32 |

    Announcements

    33 |
    34 |
    35 | {#each days as [day, announcements] (day)} 36 |
    37 |
    38 |
    {day}
    39 | {#each announcements as announcement (announcement.message)} 40 |
    41 |
    42 |
    43 |
    44 | 54 |
    55 |
    56 |
    57 |
    67 | {announcement.message} 68 |
    69 | 72 |
    73 | {/each} 74 |
    75 |
    76 | {/each} 77 |
    78 |
    79 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 118 | 119 | {#if loading} 120 | 121 | {:else} 122 |
    123 | {#if gatusConfig.announcements.length > 0} 124 | 125 | {/if} 126 | 127 | {#each groups as group (group.title)} 128 | 133 | {/each} 134 | 138 |