diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..2523798 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,16 @@ +name: Lint + +on: + push: + branches: [main] + pull_request: + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + - run: bun install --frozen-lockfile + - run: bun run lint + - run: bun run format:check diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..ea5a55b --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +bunx lint-staged diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..b7f88e5 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,42 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "jsx-a11y", "import", "typescript", "oxc"], + "env": { + "browser": true, + "node": true, + "es2024": true + }, + "settings": { + "react": { + "version": "19" + } + }, + "categories": { + "correctness": "error", + "suspicious": "warn" + }, + "rules": { + "react/react-in-jsx-scope": "off" + }, + "overrides": [ + { + "files": ["src/client/main.tsx"], + "rules": { + "import/no-unassigned-import": "off" + } + }, + { + "files": ["src/client/components/charts/*.tsx"], + "rules": { + "jsx-a11y/prefer-tag-over-role": "off" + } + }, + { + "files": ["src/client/components/StatusHistoryBar.tsx"], + "rules": { + "react/capitalized-calls": "off" + } + } + ], + "ignorePatterns": ["dist/**", ".wrangler/**", "worker-configuration.d.ts", "migrations/**", "node_modules/**"] +} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..70393fb --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +dist +.wrangler +node_modules +bun.lock +worker-configuration.d.ts +migrations diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..41aa4f1 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["esbenp.prettier-vscode", "oxc.oxc-vscode", "EditorConfig.EditorConfig"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 0126e59..febd763 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,25 @@ { "files.associations": { "wrangler.json": "jsonc" + }, + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.fixAll.oxc": "explicit" + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[jsonc]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[css]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" } -} \ No newline at end of file +} diff --git a/DESIGN.md b/DESIGN.md index c784b1b..64617ef 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -232,6 +232,7 @@ Typography runs **Circular** at weight 500 for display and 400 for body. The dis The product itself appears as composited UI screenshots on every page: dashboard tables, SQL editors, query builders, log streams. These screenshots are the brand's argument. They sit inside `{rounded.lg}` 12px containers with subtle 1px hairlines, often arranged 2-up or in a floating "stacked panes" composition above the hero band. **Key Characteristics:** + - Single emerald primary (`{colors.primary}` `#3ecf8e`) as the only chromatic event; everything else is monochrome. - White canvas marketing track with greyscale hierarchy from `{colors.hairline-cool}` to `{colors.ink}`. - Custom humanist sans display tier at weight 500 with negative letter-spacing of -1.92px to -0.42px. @@ -245,6 +246,7 @@ The product itself appears as composited UI screenshots on every page: dashboard > **Source pages:** home (`/`), `/database`, `/partners/integrations`, `/partners/integrations/powersync`, `/solutions/ai-builders`, `/pricing`. ### Brand & Accent + - **Emerald** (`{colors.primary}` — `#3ecf8e`): The signature CTA color. Filled-button background, brand wordmark accent, dot indicator. - **Emerald Deep** (`{colors.primary-deep}` — `#24b47e`): Pressed-state lift of the primary. - **Emerald Soft** (`{colors.primary-soft}` — `#4ade80`): Lighter emerald used in chart accents and product UI. @@ -254,6 +256,7 @@ The product itself appears as composited UI screenshots on every page: dashboard - **Accent Pink / Crimson / Indigo / Tomato**: Reserved for integration logos and rare chart highlights, never as system colors. ### Surface + - **Canvas** (`{colors.canvas}` — `#ffffff`): Default page background. - **Canvas Soft** (`{colors.canvas-soft}` — `#fafafa`): Barely-tinted off-white for alternating section bands. - **Canvas Night** (`{colors.canvas-night}` — `#1c1c1c`): Deep near-black used in code blocks, dashboard mockups, featured pricing tier. @@ -263,6 +266,7 @@ The product itself appears as composited UI screenshots on every page: dashboard - **Hairline Cool** (`{colors.hairline-cool}` — `#ededed`) / **Hairline Cool 2** (`#efefef`) / **Hairline Cool 3** (`#d4d4d4`): The brand's grey ladder for fine chrome work. ### Text + - **Ink** (`{colors.ink}` — `#171717`): Default body text. Near-black, never pure. - **Ink Secondary** (`{colors.ink-secondary}` — `#212121`): Slightly cooler near-black for body emphasis. - **Ink Mute** (`{colors.ink-mute}` — `#707070`): Secondary text and helper copy. @@ -283,71 +287,78 @@ Code blocks use **system mono** (`ui-monospace`, with Menlo / Monaco / Consolas ### Hierarchy -| Token | Size | Weight | Line Height | Letter Spacing | Use | -|---|---|---|---|---|---| -| `{typography.display-xxl}` | 64px | 500 | 1.1 | -1.92px | Hero headline | -| `{typography.display-xl}` | 48px | 500 | 1.1 | -1.44px | Section opener | -| `{typography.display-lg}` | 36px | 500 | 1.15 | -0.72px | Sub-section / pricing tier | -| `{typography.display-md}` | 28px | 500 | 1.2 | -0.42px | Card title | -| `{typography.heading-lg}` | 22px | 500 | 1.2 | 0 | Compact heading | -| `{typography.heading-md}` | 18px | 500 | 1.4 | 0 | Section sub-heading | -| `{typography.body-lg}` | 18px | 400 | 1.55 | 0 | Marketing body lead | -| `{typography.body-md}` | 16px | 400 | 1.5 | 0 | Default UI body | -| `{typography.button-md}` | 14px | 500 | 1.0 | 0 | Button label | -| `{typography.caption}` | 13px | 400 | 1.45 | 0 | Helper, footnote | -| `{typography.micro}` | 12px | 400 | 1.45 | 0 | Pill label, fine print | -| `{typography.code}` | 14px | 400 | 1.5 | 0 | Code block content | +| Token | Size | Weight | Line Height | Letter Spacing | Use | +| -------------------------- | ---- | ------ | ----------- | -------------- | -------------------------- | +| `{typography.display-xxl}` | 64px | 500 | 1.1 | -1.92px | Hero headline | +| `{typography.display-xl}` | 48px | 500 | 1.1 | -1.44px | Section opener | +| `{typography.display-lg}` | 36px | 500 | 1.15 | -0.72px | Sub-section / pricing tier | +| `{typography.display-md}` | 28px | 500 | 1.2 | -0.42px | Card title | +| `{typography.heading-lg}` | 22px | 500 | 1.2 | 0 | Compact heading | +| `{typography.heading-md}` | 18px | 500 | 1.4 | 0 | Section sub-heading | +| `{typography.body-lg}` | 18px | 400 | 1.55 | 0 | Marketing body lead | +| `{typography.body-md}` | 16px | 400 | 1.5 | 0 | Default UI body | +| `{typography.button-md}` | 14px | 500 | 1.0 | 0 | Button label | +| `{typography.caption}` | 13px | 400 | 1.45 | 0 | Helper, footnote | +| `{typography.micro}` | 12px | 400 | 1.45 | 0 | Pill label, fine print | +| `{typography.code}` | 14px | 400 | 1.5 | 0 | Code block content | ### Principles + - **Weight 500 across display.** Mid-weight reads as engineered, not decorative. - **Negative tracking on display.** -1.92px at 64px scaling proportionally down — tightens the rounded humanist letterforms into editorial density. - **Mono for code.** System mono families (Menlo / Monaco) — no proprietary mono webfont. ### Note on Font Substitutes + Circular is proprietary. Use **Inter** at weight 500 with `letter-spacing: -1.92px` for display tiers. **Geist Sans** (open-source from Vercel) is another close alternative for both display and body. Avoid Helvetica defaults — they're heavier and lack the geometric warmth. ## Layout ### Spacing System + - **Base unit**: 8px (with 2 / 4 / 12 sub-tokens for fine work). - **Tokens**: `{spacing.xxs}` 2px · `{spacing.xs}` 4px · `{spacing.sm}` 8px · `{spacing.md}` 12px · `{spacing.lg}` 16px · `{spacing.xl}` 24px · `{spacing.xxl}` 32px · `{spacing.huge}` 64px. - **Section padding**: 64–96px on marketing surfaces. - **Card internal padding**: 32px on feature/pricing cards. ### Grid & Container + - Marketing pages center in a ~1280px container with no edge-bleed; the brand keeps content inside the box. - Pricing collapses 4-up → 2-up → 1-up at 1024 / 768 breakpoints. - Product UI mockups stack 2-up or render as overlapping panes inside the same container. ### Whitespace Philosophy + The brand uses generous 64–96px section padding without atmospheric gradients filling the space — the white canvas is the design. The composited product UI mockups break up sections without requiring decoration. ## Elevation & Depth -| Level | Treatment | Use | -|---|---|---| -| 0 | Flat, 1px hairline | Default cards | -| 1 | `box-shadow: 0 1px 3px rgba(0,0,0,0.06)` | Subtle card lift | -| 2 | `box-shadow: 0 8px 24px rgba(0,0,0,0.08)` | Floating composited UI mockups | -| 3 | `box-shadow: 0 16px 48px rgba(0,0,0,0.12)` | Modal overlays, deep elevation | +| Level | Treatment | Use | +| ----- | ------------------------------------------ | ------------------------------ | +| 0 | Flat, 1px hairline | Default cards | +| 1 | `box-shadow: 0 1px 3px rgba(0,0,0,0.06)` | Subtle card lift | +| 2 | `box-shadow: 0 8px 24px rgba(0,0,0,0.08)` | Floating composited UI mockups | +| 3 | `box-shadow: 0 16px 48px rgba(0,0,0,0.12)` | Modal overlays, deep elevation | ### Decorative Depth + The brand's depth is **product UI mockups** rather than gradients. Stacked dashboard / SQL editor / log panes composite together with subtle Level 2 shadows to suggest spatial hierarchy. ## Shapes ### Border Radius Scale -| Token | Value | Use | -|---|---|---| -| `{rounded.xs}` | 4px | Form inputs, hairline tags | -| `{rounded.sm}` | 6px | Buttons (the brand's signature button radius), code blocks | -| `{rounded.md}` | 8px | Compact cards, alerts | -| `{rounded.lg}` | 12px | Pricing cards, feature cards, product mockups | -| `{rounded.xl}` | 16px | Modal dialogs, large container chrome | -| `{rounded.full}` | 9999px | Pill tags, avatars | +| Token | Value | Use | +| ---------------- | ------ | ---------------------------------------------------------- | +| `{rounded.xs}` | 4px | Form inputs, hairline tags | +| `{rounded.sm}` | 6px | Buttons (the brand's signature button radius), code blocks | +| `{rounded.md}` | 8px | Compact cards, alerts | +| `{rounded.lg}` | 12px | Pricing cards, feature cards, product mockups | +| `{rounded.xl}` | 16px | Modal dialogs, large container chrome | +| `{rounded.full}` | 9999px | Pill tags, avatars | ### Photography Geometry + The brand uses minimal photography. Customer logo strips display wordmarks at uniform height (~24–32px) in greyscale; case-study cards (rare) use 4:3 photos inset in `{rounded.lg}` containers. ## Components @@ -355,51 +366,64 @@ The brand uses minimal photography. Customer logo strips display wordmarks at un ### Buttons **`button-primary-green`** — the signature CTA. + - Background `{colors.primary}`, text `{colors.on-primary}` (near-black, NOT white), type `{typography.button-md}`, padding `{spacing.sm} {spacing.lg}` (8px 16px), rounded `{rounded.sm}` 6px. - Pressed state `button-primary-green-pressed` shifts to `{colors.primary-deep}`. **`button-secondary-outline`** — outline alternative on white. + - Background `{colors.canvas}`, text `{colors.ink}`, 1px solid `{colors.hairline-strong}` border, same shape. **`button-on-dark`** — used on dark surfaces / code-block CTAs. + - Background `{colors.canvas-night}`, text `{colors.on-dark}`, same shape. **`button-link`** — text-only inline button. + - Transparent background, text `{colors.ink}` rendered in `{typography.button-md}`, no padding, with a subtle underline on hover. ### Cards & Containers **`card-feature-light`** — feature card on white. + - Background `{colors.canvas}`, padding `{spacing.xxl}`, rounded `{rounded.lg}` 12px, 1px `{colors.hairline}` border. **`card-pricing`** — standard pricing tier. + - Background `{colors.canvas}`, padding `{spacing.xxl}`, rounded `{rounded.lg}`, 1px `{colors.hairline}` border. Title in `{typography.heading-lg}`, price in `{typography.display-md}`, body in `{typography.body-md}`, CTA `button-primary-green` pinned bottom. **`card-pricing-featured`** — inverted dark featured tier. + - Background `{colors.canvas-night}`, text `{colors.on-dark}`, otherwise identical structure. **`card-feature-dark`** — feature card with deep dark fill. + - Background `{colors.canvas-night}`, text `{colors.on-dark}`, padding `{spacing.xxl}`, rounded `{rounded.lg}`. Used for code-heavy feature explanations. **`code-block`** — code snippet container. + - Background `{colors.canvas-night}`, text `{colors.on-dark}` rendered in `{typography.code}`. Padding `{spacing.lg}` 16px, rounded `{rounded.sm}` 6px. ### Inputs & Forms **`text-input`** — standard form input. + - Background `{colors.canvas}`, text `{colors.ink}`, type `{typography.body-md}`, padding `{spacing.sm} {spacing.md}` (8px 12px), rounded `{rounded.sm}` 6px, 1px `{colors.hairline}` border. ### Navigation **`nav-bar-light`** — top nav across the site. + - Background `{colors.canvas}`, text `{colors.ink}`, padding `{spacing.lg} {spacing.xl}`. Logo on the left, primary nav center, "Sign In" link + filled `button-primary-green` on the right. ### Pills, Tags, and Chips **`pill-tag-green`** — small green pill used for "new" or featured indicators. + - Background `{colors.primary}`, text `{colors.on-primary}`, type `{typography.micro}`, padding `{spacing.xxs} {spacing.sm}`, rounded `{rounded.full}`. **`pill-tag-soft`** — neutral pill on light surfaces. + - Background `{colors.canvas-soft}`, text `{colors.ink}`, otherwise same shape. ### Signature Components @@ -407,14 +431,17 @@ The brand uses minimal photography. Customer logo strips display wordmarks at un **Composited Product UI Mockups** — multi-layer dashboard / SQL editor / log pane composites with subtle Level 2 shadows. The product is the brand's argument; mockups always sit on white canvas with no surrounding decoration. **`link-on-light`** — inline links in body copy. + - Text `{colors.ink}` rendered in `{typography.body-md}` with a persistent underline. **`footer-light`** — site-wide footer. + - Background `{colors.canvas}`, text `{colors.ink-mute}`, type `{typography.caption}`, padding `{spacing.huge} {spacing.xl}` (64px 24px). Holds 4–5 columns of link groups, social icons, and a small legal row. ## Do's and Don'ts ### Do + - Reserve `{colors.primary}` emerald for filled CTAs and the wordmark accent — it should appear sparingly. - Render display tiers at weight 500 with negative letter-spacing — the engineered tightness is part of the brand. - Use `{rounded.sm}` 6px for buttons — square-ish radii, never pill-shaped. @@ -423,6 +450,7 @@ The brand uses minimal photography. Customer logo strips display wordmarks at un - Apply system mono for every code block. ### Don't + - Don't introduce additional accent colors as system colors — purples, yellows, and pinks belong inside chart points and integration logos only. - Don't bump display weight above 500 — the brand's calibrated mid-weight breaks at 600+. - Don't use pill-shaped buttons; the brand's button radius is square-ish 6px. @@ -433,23 +461,26 @@ The brand uses minimal photography. Customer logo strips display wordmarks at un ### Breakpoints -| Name | Width | Key Changes | -|---|---|---| -| Wide | ≥ 1440px | Full container width; product mockups at full scale | -| Desktop | 1024–1440px | Default content max-width; pricing 4-up | -| Tablet | 768–1023px | Pricing 2-up; mockups simplify to single panel | -| Mobile | < 768px | Pricing 1-up; hamburger nav; display drops 64 → 36px | +| Name | Width | Key Changes | +| ------- | ----------- | ---------------------------------------------------- | +| Wide | ≥ 1440px | Full container width; product mockups at full scale | +| Desktop | 1024–1440px | Default content max-width; pricing 4-up | +| Tablet | 768–1023px | Pricing 2-up; mockups simplify to single panel | +| Mobile | < 768px | Pricing 1-up; hamburger nav; display drops 64 → 36px | ### Touch Targets + - Buttons hit ≥ 36×36px on mobile; vertical padding scales up to maintain WCAG AA minimum. - Form fields stay at 36px minimum height. ### Collapsing Strategy + - Display tiers stair-step 64 → 48 → 36 → 28 → 22px. - Product UI mockups simplify to a single primary panel on mobile. - Pricing tiers stair-step 4-up → 2-up → 1-up; dark featured tier always distinguished. ### Image Behavior + Product UI mockups use `srcset` with desktop / mobile crops; mobile crops focus on the most actionable inner panel. ## Iteration Guide diff --git a/bun.lock b/bun.lock index c0ea580..5bc3623 100644 --- a/bun.lock +++ b/bun.lock @@ -22,6 +22,10 @@ "@types/react-dom": "^19.2.5", "@vitejs/plugin-react": "^6.1.0", "drizzle-kit": "^0.31.10", + "husky": "^9.1.7", + "lint-staged": "^17.4.1", + "oxlint": "^1.80.0", + "prettier": "^3.9.6", "tailwindcss": "4", "typescript": "^5.5.2", "vite": "^8.2.2", @@ -181,6 +185,44 @@ "@oxc-project/types": ["@oxc-project/types@0.147.0", "", {}, "sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg=="], + "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.80.0", "", { "os": "android", "cpu": "arm" }, "sha512-RM3Plj+biQpxa5d1GOOX6ciDlcUROmm4OZ/pLTpitkQt2mJv4jhtY4cbgaetOm5UKWZe05/TGQ6o1Vl8EOHkrA=="], + + "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.80.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YlO5JEf0Yr2bUUlu8O8daVcUxtcGGbcSmyV7E7nSbJbfAdxTE0PFPwgnIlw7wXJaTYjb+qs5hI5q3jxUkI7cAw=="], + + "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.80.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BULDOyO3AhsmdWfQeIUCykDt3dd7XZBGLhp1eIh56skRv01O+cNjNPwXMIbeW1x4+pxcln5if72wcRgViVo7PA=="], + + "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.80.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-YJ4JzLw7N5TDSQFlA0hAQGHvnDZgyypm1yunObVWcWiF9KM7eGCJKYKLgTC2Fi/57OdnBhbj4OkzPGdFQJ6HyA=="], + + "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.80.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-AYUIk5QnL0s8oWAYsREZwkRYy1SupJTXALo93J1TgzHywxQtdM99FecRMQ87MXEdPQ0j1TmEpeeq3fGNkpvMqg=="], + + "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.80.0", "", { "os": "linux", "cpu": "arm" }, "sha512-9hBZVANupQ89W9dXyE0n8doCyaW5pDyGn3y6XlIMPZ+rIKuyqkr3SNUXmVJIhuvUq0NBU3RBiSXXE69l4XI6KA=="], + + "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.80.0", "", { "os": "linux", "cpu": "arm" }, "sha512-SvS2uKqzY+pbfuvAHzH4338R6Zwo805GAwrIMVvK1KxoOWCIjZUdfzTCvilD7z6JK91v011+zYMryabhDo2AsQ=="], + + "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.80.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-tCLadyqRVL3pQTRPNg7cjXKvcvS4fbyXeQHhKk5BTJ1oftQln5/yIIWbu/Xom/DX41zv2P9QGt6+D/TtQVtY3A=="], + + "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.80.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-XfpCNRlOPcLlJl4Bn/FUhjqlR6BVavEykERBf/MV7YA9VZDa5g5znVqYhyviMafcxS9Pe/i/kPvHNO0U6svEHQ=="], + + "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.80.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3I4yMwcFG9NeO8ioY6JBBuKsIm5GL/x7MATt1S4tVWaxPu5HcJ+XnLUbcVBTxG8q2Wu56HSj+NmXQiVYb1lp6A=="], + + "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.80.0", "", { "os": "linux", "cpu": "none" }, "sha512-E1wAKymkpe1/E8helzBKdm81OBOF+ezxRyXRMEuik3ZpWDER5CPOKZwF66RsdwW98uwZv8UTFremUQtC1CzdJA=="], + + "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.80.0", "", { "os": "linux", "cpu": "none" }, "sha512-+gLRGD4sIo3+VA++iham5UxD9tKSoJ/VOrROCEXIcknrYtQg6iIQgvjN0cpiRF7N6UYC7pJbvHJlDnMge5LRpQ=="], + + "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.80.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-aR0PrzHj9leW3NmzBAAP4EzdoBNoJcs9sjnIQPIwyRnBGYrRbXUIpEB5Q39AqK3PLY5JK5uEhDQDiUa1QSAstw=="], + + "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.80.0", "", { "os": "linux", "cpu": "x64" }, "sha512-vSVh5cSo3Xxs6ghBCcFJlpbkbENzDog1qXtoXLa/HC3aCrR4XO76GZbXmQoCPHnu99nQpdCeC3H9tdNICfDh7A=="], + + "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.80.0", "", { "os": "linux", "cpu": "x64" }, "sha512-FfzBXpNQ8u7/ZI/p8bl73MeZ508Ax3hxWp3SiJpEFiC+BB9XcXy5FAZHTLKDPSzrUpxQZSZJAVdDmuJp/+HDBQ=="], + + "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.80.0", "", { "os": "none", "cpu": "arm64" }, "sha512-zMzbkumtmprCgRwoYNzcB3iC39fXdJIMLMU33KdCjEGLlJGOEt1+LwQ4LF8ndLzAEKVz4BR0y3V6Xrkk3Nm3yA=="], + + "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.80.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-ib6iRcrXsk4t1fm3iKcwksyWh1ZkZXC/2mEzakl0ai2+6HZunf1WWMZ/xP9EJAvw9g9K4UVTC3NF/+G2qLrbTQ=="], + + "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.80.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-xhRWBMpLxZvgKAH6+DJZmpP+W8Y8UdQOSU1JfxSWNXsaBaRGW77j+1hCuNHlzj7OH4SPN8fYd1q0o2qrDtoVyw=="], + + "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.80.0", "", { "os": "win32", "cpu": "x64" }, "sha512-yAnO7lwBYQnz2pcfBPIGQQZWIX5zd5R/1aAKIF3oE+TVj7IhoHcROjOkz3sRDngzqhfPKfFaXqug5j5rE5dn6Q=="], + "@poppinss/colors": ["@poppinss/colors@4.1.6", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg=="], "@poppinss/dumper": ["@poppinss/dumper@0.6.5", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@sindresorhus/is": "^7.0.2", "supports-color": "^10.0.0" } }, "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw=="], @@ -385,6 +427,8 @@ "hono": ["hono@4.13.5", "", {}, "sha512-O6+/eCYRkzzzy0rPWwKLiGBR1nFuUPZynnwjxN1MBA62NNqbT0wQEzQyK2gSO5yDIDB336sXQleAhOHrzlYyKw=="], + "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], + "immer": ["immer@11.1.18", "", {}, "sha512-EQyQtLiYW029lyoczMl/Hh4Xu7cDecSc58JRYpHyL4tIAu3eqd1yJzQX04d2BZHDkzFFvm6qJEJWOtfDSWAXbQ=="], "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], @@ -417,6 +461,8 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="], + "lint-staged": ["lint-staged@17.4.1", "", { "dependencies": { "picomatch": "^4.0.7", "string-argv": "^0.3.2", "tinyexec": "^1.3.0" }, "optionalDependencies": { "yaml": "^2.9.0" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-FmJeudcalbSfg1du+JCfvi5vS6Qt08KgbfLWiHinbef+2JJwUZwAWVoaO1AcJVUTWPfk0t30PMQNwPAeCzYQ+Q=="], + "lucide-react": ["lucide-react@1.34.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-vnjGJNI7Htk5+oWW8gXGuaLgwgAb0T6/iZbBrp9JCfRFwdNWZ0YTm3eyxjOLgwN6r8iyAf3UA70zNmBRBNv7yg=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], @@ -427,6 +473,8 @@ "obug": ["obug@2.1.4", "", {}, "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA=="], + "oxlint": ["oxlint@1.80.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.80.0", "@oxlint/binding-android-arm64": "1.80.0", "@oxlint/binding-darwin-arm64": "1.80.0", "@oxlint/binding-darwin-x64": "1.80.0", "@oxlint/binding-freebsd-x64": "1.80.0", "@oxlint/binding-linux-arm-gnueabihf": "1.80.0", "@oxlint/binding-linux-arm-musleabihf": "1.80.0", "@oxlint/binding-linux-arm64-gnu": "1.80.0", "@oxlint/binding-linux-arm64-musl": "1.80.0", "@oxlint/binding-linux-ppc64-gnu": "1.80.0", "@oxlint/binding-linux-riscv64-gnu": "1.80.0", "@oxlint/binding-linux-riscv64-musl": "1.80.0", "@oxlint/binding-linux-s390x-gnu": "1.80.0", "@oxlint/binding-linux-x64-gnu": "1.80.0", "@oxlint/binding-linux-x64-musl": "1.80.0", "@oxlint/binding-openharmony-arm64": "1.80.0", "@oxlint/binding-win32-arm64-msvc": "1.80.0", "@oxlint/binding-win32-ia32-msvc": "1.80.0", "@oxlint/binding-win32-x64-msvc": "1.80.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-5nTiSps4qdbCWLbxzuO00alHkEO2exR9YMN/ig6QXWrLsYSG0KaObOAM+l6oU2LcKPWoSAGYbkZIGEu1ViiWKA=="], + "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], @@ -437,6 +485,8 @@ "postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], + "prettier": ["prettier@3.9.6", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g=="], + "react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="], "react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="], @@ -475,6 +525,8 @@ "std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="], + "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], + "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], "tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="], @@ -519,6 +571,8 @@ "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], + "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], + "youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="], "youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="], diff --git a/drizzle.config.ts b/drizzle.config.ts index f83ab8b..cdffd2c 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,7 +1,7 @@ -import { defineConfig } from "drizzle-kit"; +import { defineConfig } from 'drizzle-kit'; export default defineConfig({ - dialect: "sqlite", - schema: "./src/worker/db/schema.ts", - out: "./migrations", + dialect: 'sqlite', + schema: './src/worker/db/schema.ts', + out: './migrations', }); diff --git a/index.html b/index.html index dea5739..dbbd881 100644 --- a/index.html +++ b/index.html @@ -3,10 +3,7 @@ - + Upwatch — Uptime monitoring diff --git a/package.json b/package.json index 630fcf5..a64dd3a 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,12 @@ "types": "wrangler types", "db:local": "wrangler d1 migrations apply uptime --local", "db:remote": "wrangler d1 migrations apply uptime --remote", - "admin:create": "bun scripts/create-admin.ts" + "admin:create": "bun scripts/create-admin.ts", + "lint": "oxlint", + "lint:fix": "oxlint --fix", + "format": "prettier --write .", + "format:check": "prettier --check .", + "prepare": "husky" }, "devDependencies": { "@cloudflare/vite-plugin": "^1.54.1", @@ -22,6 +27,10 @@ "@types/react-dom": "^19.2.5", "@vitejs/plugin-react": "^6.1.0", "drizzle-kit": "^0.31.10", + "husky": "^9.1.7", + "lint-staged": "^17.4.1", + "oxlint": "^1.80.0", + "prettier": "^3.9.6", "tailwindcss": "4", "typescript": "^5.5.2", "vite": "^8.2.2", @@ -40,5 +49,14 @@ "trustedDependencies": [ "workerd", "esbuild" - ] + ], + "lint-staged": { + "*.{ts,tsx}": [ + "oxlint --fix", + "prettier --write" + ], + "*.{js,jsx,json,jsonc,css,md}": [ + "prettier --write" + ] + } } diff --git a/scripts/create-admin.ts b/scripts/create-admin.ts index 0508cb7..1369fdf 100644 --- a/scripts/create-admin.ts +++ b/scripts/create-admin.ts @@ -1,35 +1,35 @@ -import { hashPassword } from "../src/worker/lib/password"; +import { hashPassword } from '../src/worker/lib/password'; async function readPassword() { if (!process.stdin.isTTY) { return (await new Response(Bun.stdin.stream()).text()).trimEnd(); } - process.stdout.write("Password: "); + process.stdout.write('Password: '); process.stdin.setRawMode(true); process.stdin.resume(); - process.stdin.setEncoding("utf8"); + process.stdin.setEncoding('utf8'); return new Promise((resolve, reject) => { - let password = ""; + let password = ''; const cleanup = () => { process.stdin.setRawMode(false); process.stdin.pause(); - process.stdout.write("\n"); + process.stdout.write('\n'); }; - process.stdin.on("data", (key: string) => { - if (key === "\u0003") { + process.stdin.on('data', (key: string) => { + if (key === '\u0003') { cleanup(); - reject(new Error("Cancelled")); + reject(new Error('Cancelled')); return; } - if (key === "\r" || key === "\n") { + if (key === '\r' || key === '\n') { cleanup(); resolve(password); return; } - if (key === "\u007f" || key === "\b") { + if (key === '\u007f' || key === '\b') { password = password.slice(0, -1); return; } @@ -43,23 +43,23 @@ function sqlValue(value: string) { } function shellDoubleQuoted(value: string) { - return value.replace(/[\\"$`]/g, "\\$&"); + return value.replace(/[\\"$`]/g, '\\$&'); } const password = await readPassword(); if (password.length < 8) { - console.error("Password must contain at least 8 characters."); + console.error('Password must contain at least 8 characters.'); process.exit(1); } const now = Date.now(); const statement = [ - "INSERT OR REPLACE INTO admin_credentials", - "(id, password_hash, created_at, updated_at)", + 'INSERT OR REPLACE INTO admin_credentials', + '(id, password_hash, created_at, updated_at)', `VALUES (1, ${sqlValue(await hashPassword(password))}, ${now}, ${now});`, -].join(" "); +].join(' '); -console.log("\nLocal database:"); +console.log('\nLocal database:'); console.log(`bunx wrangler d1 execute uptime --local --command "${shellDoubleQuoted(statement)}"`); -console.log("\nRemote database:"); +console.log('\nRemote database:'); console.log(`bunx wrangler d1 execute uptime --remote --command "${shellDoubleQuoted(statement)}"`); diff --git a/src/client/App.tsx b/src/client/App.tsx index 8407f78..fbfe00b 100644 --- a/src/client/App.tsx +++ b/src/client/App.tsx @@ -1,26 +1,24 @@ -import { RequireAuth } from "./components/RequireAuth"; -import { usePathname } from "./lib/router"; -import { DashboardPage } from "./pages/DashboardPage"; -import { LoginPage } from "./pages/LoginPage"; -import { MonitorDetailPage } from "./pages/MonitorDetailPage"; -import { SettingsPage } from "./pages/SettingsPage"; -import { StatusPage } from "./pages/StatusPage"; +import { RequireAuth } from './components/RequireAuth'; +import { usePathname } from './lib/router'; +import { DashboardPage } from './pages/DashboardPage'; +import { LoginPage } from './pages/LoginPage'; +import { MonitorDetailPage } from './pages/MonitorDetailPage'; +import { SettingsPage } from './pages/SettingsPage'; +import { StatusPage } from './pages/StatusPage'; function App() { const pathname = usePathname(); - if (pathname === "/") return ; - if (pathname === "/login") return ; + if (pathname === '/') return ; + if (pathname === '/login') return ; const monitorMatch = pathname.match(/^\/monitors\/(\d+)\/?$/); - const page = monitorMatch - ? - : pathname === "/settings" - ? - : ; - return ( - - {page} - + const page = monitorMatch ? ( + + ) : pathname === '/settings' ? ( + + ) : ( + ); + return {page}; } export default App; diff --git a/src/client/api/auth.ts b/src/client/api/auth.ts index 152b63a..f5a1592 100644 --- a/src/client/api/auth.ts +++ b/src/client/api/auth.ts @@ -1,4 +1,4 @@ -import { getJson, postJson } from "./http"; +import { getJson, postJson } from './http'; export type SessionResponse = { authenticated: boolean; @@ -9,16 +9,16 @@ export type LoginInput = { }; export function getSession(signal?: AbortSignal) { - return getJson("/api/auth/me", { + return getJson('/api/auth/me', { signal, - credentials: "same-origin", + credentials: 'same-origin', }); } export function login(input: LoginInput) { - return postJson("/api/auth/login", input); + return postJson('/api/auth/login', input); } export function logout() { - return postJson<{ ok: true }>("/api/auth/logout"); + return postJson<{ ok: true }>('/api/auth/logout'); } diff --git a/src/client/api/health.ts b/src/client/api/health.ts index 053a085..3d5be9e 100644 --- a/src/client/api/health.ts +++ b/src/client/api/health.ts @@ -1,4 +1,4 @@ -import { getJson } from "./http"; +import { getJson } from './http'; export type HealthResponse = { ok: boolean; @@ -7,5 +7,5 @@ export type HealthResponse = { }; export function getHealth(signal?: AbortSignal) { - return getJson("/api/health", { signal }); + return getJson('/api/health', { signal }); } diff --git a/src/client/api/http.ts b/src/client/api/http.ts index 7893ebe..611487e 100644 --- a/src/client/api/http.ts +++ b/src/client/api/http.ts @@ -4,14 +4,14 @@ export class ApiError extends Error { readonly status: number, ) { super(message); - this.name = "ApiError"; + this.name = 'ApiError'; } } async function getErrorMessage(response: Response) { try { const body = await response.json<{ message?: unknown }>(); - if (typeof body.message === "string" && body.message.length > 0) { + if (typeof body.message === 'string' && body.message.length > 0) { return body.message; } } catch { @@ -21,12 +21,9 @@ async function getErrorMessage(response: Response) { return `Request returned HTTP ${response.status}`; } -export async function getJson( - input: RequestInfo | URL, - init: RequestInit = {}, -): Promise { +export async function getJson(input: RequestInfo | URL, init: RequestInit = {}): Promise { const headers = new Headers(init.headers); - headers.set("Accept", "application/json"); + headers.set('Accept', 'application/json'); const response = await fetch(input, { ...init, @@ -40,51 +37,31 @@ export async function getJson( return response.json() as Promise; } -function sendJson( - method: "POST" | "PUT" | "PATCH" | "DELETE", - input: RequestInfo | URL, - body?: unknown, - init: RequestInit = {}, -) { +function sendJson(method: 'POST' | 'PUT' | 'PATCH' | 'DELETE', input: RequestInfo | URL, body?: unknown, init: RequestInit = {}) { const headers = new Headers(init.headers); - headers.set("Content-Type", "application/json"); + headers.set('Content-Type', 'application/json'); return getJson(input, { ...init, method, headers, - credentials: "same-origin", + credentials: 'same-origin', body: body === undefined ? undefined : JSON.stringify(body), }); } -export function postJson( - input: RequestInfo | URL, - body?: unknown, - init: RequestInit = {}, -) { - return sendJson("POST", input, body, init); +export function postJson(input: RequestInfo | URL, body?: unknown, init: RequestInit = {}) { + return sendJson('POST', input, body, init); } -export function patchJson( - input: RequestInfo | URL, - body?: unknown, - init: RequestInit = {}, -) { - return sendJson("PATCH", input, body, init); +export function patchJson(input: RequestInfo | URL, body?: unknown, init: RequestInit = {}) { + return sendJson('PATCH', input, body, init); } -export function putJson( - input: RequestInfo | URL, - body?: unknown, - init: RequestInit = {}, -) { - return sendJson("PUT", input, body, init); +export function putJson(input: RequestInfo | URL, body?: unknown, init: RequestInit = {}) { + return sendJson('PUT', input, body, init); } -export function deleteJson( - input: RequestInfo | URL, - init: RequestInit = {}, -) { - return sendJson("DELETE", input, undefined, init); +export function deleteJson(input: RequestInfo | URL, init: RequestInit = {}) { + return sendJson('DELETE', input, undefined, init); } diff --git a/src/client/api/monitors.ts b/src/client/api/monitors.ts index 9937244..ff2c527 100644 --- a/src/client/api/monitors.ts +++ b/src/client/api/monitors.ts @@ -1,6 +1,6 @@ -import { deleteJson, getJson, patchJson, postJson } from "./http"; +import { deleteJson, getJson, patchJson, postJson } from './http'; -export type MonitorMethod = "GET" | "HEAD" | "POST"; +export type MonitorMethod = 'GET' | 'HEAD' | 'POST'; export type Monitor = { id: number; @@ -66,34 +66,34 @@ export type StatsWindow = { }; export type MonitorStats = { - windows: Record<"24h" | "7d" | "30d" | "90d", StatsWindow>; + windows: Record<'24h' | '7d' | '30d' | '90d', StatsWindow>; }; export function listMonitors(signal?: AbortSignal) { - return getJson<{ monitors: Monitor[] }>("/api/monitors", { + return getJson<{ monitors: Monitor[] }>('/api/monitors', { signal, - credentials: "same-origin", + credentials: 'same-origin', }); } export function getMonitor(id: number, signal?: AbortSignal) { - return getJson<{ monitor: Monitor }>(`/api/monitors/${id}`, { signal, credentials: "same-origin" }); + return getJson<{ monitor: Monitor }>(`/api/monitors/${id}`, { signal, credentials: 'same-origin' }); } export function listChecks(id: number, limit = 100, signal?: AbortSignal) { - return getJson<{ checks: Check[] }>(`/api/monitors/${id}/checks?limit=${limit}`, { signal, credentials: "same-origin" }); + return getJson<{ checks: Check[] }>(`/api/monitors/${id}/checks?limit=${limit}`, { signal, credentials: 'same-origin' }); } export function getMonitorStats(id: number, signal?: AbortSignal) { - return getJson(`/api/monitors/${id}/stats`, { signal, credentials: "same-origin" }); + return getJson(`/api/monitors/${id}/stats`, { signal, credentials: 'same-origin' }); } export function listIncidents(id: number, limit = 50, signal?: AbortSignal) { - return getJson<{ incidents: Incident[] }>(`/api/monitors/${id}/incidents?limit=${limit}`, { signal, credentials: "same-origin" }); + return getJson<{ incidents: Incident[] }>(`/api/monitors/${id}/incidents?limit=${limit}`, { signal, credentials: 'same-origin' }); } export function createMonitor(input: MonitorInput) { - return postJson<{ monitor: Monitor }>("/api/monitors", input); + return postJson<{ monitor: Monitor }>('/api/monitors', input); } export function updateMonitor(id: number, input: Partial) { diff --git a/src/client/api/settings.ts b/src/client/api/settings.ts index a5f2b04..3662ffc 100644 --- a/src/client/api/settings.ts +++ b/src/client/api/settings.ts @@ -1,4 +1,4 @@ -import { getJson, postJson, putJson } from "./http"; +import { getJson, postJson, putJson } from './http'; export type NotificationSettings = { id: number; @@ -8,19 +8,19 @@ export type NotificationSettings = { updatedAt: string | null; }; -export type NotificationSettingsInput = Pick; +export type NotificationSettingsInput = Pick; export function getNotificationSettings(signal?: AbortSignal) { - return getJson<{ settings: NotificationSettings }>("/api/settings/notifications", { + return getJson<{ settings: NotificationSettings }>('/api/settings/notifications', { signal, - credentials: "same-origin", + credentials: 'same-origin', }); } export function updateNotificationSettings(input: NotificationSettingsInput) { - return putJson<{ settings: NotificationSettings }>("/api/settings/notifications", input); + return putJson<{ settings: NotificationSettings }>('/api/settings/notifications', input); } export function testNotificationWebhook() { - return postJson<{ ok: true }>("/api/settings/notifications/test"); + return postJson<{ ok: true }>('/api/settings/notifications/test'); } diff --git a/src/client/api/status.ts b/src/client/api/status.ts index f7bea93..fdf9ca0 100644 --- a/src/client/api/status.ts +++ b/src/client/api/status.ts @@ -1,7 +1,7 @@ -import { getJson } from "./http"; +import { getJson } from './http'; -export type PublicServiceStatus = "up" | "down" | "unknown"; -export type PublicOverallStatus = "operational" | "degraded" | "down"; +export type PublicServiceStatus = 'up' | 'down' | 'unknown'; +export type PublicOverallStatus = 'operational' | 'degraded' | 'down'; export type PublicService = { id: number; @@ -22,5 +22,5 @@ export type PublicStatus = { }; export function getStatus(signal?: AbortSignal) { - return getJson("/api/status", { signal }); + return getJson('/api/status', { signal }); } diff --git a/src/client/components/RequireAuth.tsx b/src/client/components/RequireAuth.tsx index 4ef6f8e..29ccba4 100644 --- a/src/client/components/RequireAuth.tsx +++ b/src/client/components/RequireAuth.tsx @@ -1,7 +1,7 @@ -import { type ReactNode, useEffect } from "react"; -import { Zap } from "lucide-react"; -import { navigate } from "../lib/router"; -import { useSessionQuery } from "../queries/auth"; +import { type ReactNode, useEffect } from 'react'; +import { Zap } from 'lucide-react'; +import { navigate } from '../lib/router'; +import { useSessionQuery } from '../queries/auth'; function FullPageLoading() { return ( @@ -18,7 +18,7 @@ export function RequireAuth({ children }: { children: ReactNode }) { useEffect(() => { if (!sessionQuery.isPending && !authenticated) { - navigate("/login", { replace: true }); + navigate('/login', { replace: true }); } }, [sessionQuery.isPending, authenticated]); diff --git a/src/client/components/SiteIcon.tsx b/src/client/components/SiteIcon.tsx index 8b3ec3b..e0518c5 100644 --- a/src/client/components/SiteIcon.tsx +++ b/src/client/components/SiteIcon.tsx @@ -1,21 +1,19 @@ -import { Database } from "lucide-react"; -import { useState } from "react"; +import { Database } from 'lucide-react'; +import { useState } from 'react'; type SiteIconProps = { monitorId: number; - favicon?: "admin" | "public"; + favicon?: 'admin' | 'public'; }; -export function SiteIcon({ monitorId, favicon = "admin" }: SiteIconProps) { +export function SiteIcon({ monitorId, favicon = 'admin' }: SiteIconProps) { const [failed, setFailed] = useState(false); if (failed) return