Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 19 additions & 13 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Specify the Node.js version to use
ARG NODE_VERSION=21
ARG NODE_VERSION=25

# Specify the Debian version to use, the default is "bullseye"
ARG DEBIAN_VERSION=bullseye
# Specify the Debian version to use, the default is "trixie".
ARG DEBIAN_VERSION=trixie

# Use Node.js Docker image as the base image, with specific Node and Debian versions
FROM node:${NODE_VERSION}-${DEBIAN_VERSION} AS build
FROM node:${NODE_VERSION}-slim AS build

# Set the container's default shell to Bash and enable some options
SHELL ["/bin/bash", "-euo", "pipefail", "-c"]
Expand All @@ -16,7 +16,7 @@ RUN apt-get update -qq --fix-missing && \
wget --quiet --output-document=- https://dl-ssl.google.com/linux/linux_signing_key.pub | gpg --dearmor > /etc/apt/trusted.gpg.d/google-archive.gpg && \
echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google.list && \
apt-get update -qq && \
apt-get -qqy --no-install-recommends install chromium traceroute python make g++ && \
apt-get -qqy --no-install-recommends install chromium traceroute python3 make g++ && \
rm -rf /var/lib/apt/lists/*

# Run the Chromium browser's version command and redirect its output to the /etc/chromium-version file
Expand All @@ -29,18 +29,22 @@ WORKDIR /app
COPY package.json yarn.lock ./

# Run yarn install to install dependencies and clear yarn cache
RUN apt-get update && \
yarn install --frozen-lockfile --network-timeout 100000 && \
rm -rf /app/node_modules/.cache
RUN yarn install --frozen-lockfile --network-timeout 100000 && \
yarn cache clean

# Copy all files to working directory
COPY . .

# Set build-time environment variables for Astro
ENV SITE_URL=http://localhost:3000 \
PLATFORM=node \
OUTPUT=hybrid

# Run yarn build to build the application
RUN yarn build --production
RUN yarn build

# Final stage
FROM node:${NODE_VERSION}-${DEBIAN_VERSION} AS final
FROM node:${NODE_VERSION}-slim AS final

WORKDIR /app

Expand All @@ -56,7 +60,9 @@ RUN apt-get update && \
EXPOSE ${PORT:-3000}

# Set the environment variable CHROME_PATH to specify the path to the Chromium binaries
ENV CHROME_PATH='/usr/bin/chromium'
# Set the environment variable PUPPETEER_EXECUTABLE_PATH to specify the path to the Chromium binaries (used by Wappalyzer)
ENV CHROME_PATH='/usr/bin/chromium' \
PUPPETEER_EXECUTABLE_PATH='/usr/bin/chromium'

# Define the command executed when the container starts and start the server.js of the Node.js application
CMD ["yarn", "start"]
# Use node directly instead of yarn for faster startup
CMD ["node", "server.js"]
8 changes: 7 additions & 1 deletion api/cookies.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@ import middleware from './_common/middleware.js';
const getPuppeteerCookies = async (url) => {
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox'],
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--single-process',
],
executablePath: process.env.CHROME_PATH || '/usr/bin/chromium',
});

try {
Expand Down
2 changes: 1 addition & 1 deletion api/features.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const featuresHandler = async (url) => {
}

if (!apiKey) {
throw new Error('Missing BuiltWith API key in environment variables');
return { skipped: 'BuiltWith API key not configured, please set BUILT_WITH_API_KEY environment variable' };
}

const apiUrl = `https://api.builtwith.com/free1/api.json?KEY=${apiKey}&LOOKUP=${encodeURIComponent(url)}`;
Expand Down
2 changes: 1 addition & 1 deletion api/linked-pages.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import axios from 'axios';
import cheerio from 'cheerio';
import * as cheerio from 'cheerio';
import urlLib from 'url';
import middleware from './_common/middleware.js';

Expand Down
3 changes: 1 addition & 2 deletions api/mail-config.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import dns from 'dns';
import dns from 'dns/promises';
import URL from 'url-parse';
import middleware from './_common/middleware.js';

// TODO: Fix.

const mailConfigHandler = async (url, event, context) => {
try {
Expand Down
4 changes: 1 addition & 3 deletions api/quality.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ const qualityHandler = async (url, event, context) => {
const apiKey = process.env.GOOGLE_CLOUD_API_KEY;

if (!apiKey) {
throw new Error(
'Missing Google API. You need to set the `GOOGLE_CLOUD_API_KEY` environment variable'
);
return { skipped: 'Google API key not configured. Please export GOOGLE_CLOUD_API_KEY' };
}

const endpoint = `https://www.googleapis.com/pagespeedonline/v5/runPagespeed?`
Expand Down
11 changes: 8 additions & 3 deletions api/screenshot.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import middleware from './_common/middleware.js';
import { execFile } from 'child_process';
import { promises as fs } from 'fs';
import path from 'path';
import pkg from 'uuid';
const { v4: uuidv4 } = pkg;
import { v4 as uuidv4 } from 'uuid';

// Helper function for direct chromium screenshot as fallback
const directChromiumScreenshot = async (url) => {
Expand Down Expand Up @@ -92,7 +91,13 @@ const screenshotHandler = async (targetUrl) => {
try {
console.log(`[SCREENSHOT] Launching puppeteer browser`);
browser = await puppeteer.launch({
args: [...chromium.args, '--no-sandbox'], // Add --no-sandbox flag
args: [
...chromium.args,
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--single-process',
],
defaultViewport: { width: 800, height: 600 },
executablePath: process.env.CHROME_PATH || '/usr/bin/chromium',
headless: true,
Expand Down
4 changes: 3 additions & 1 deletion api/sitemap.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const sitemapHandler = async (url) => {
}

if (!sitemapUrl) {
return { skipped: 'No sitemap found' };
return { notApplicable: 'No sitemap.xml found on this domain' };
}

sitemapRes = await axios.get(sitemapUrl, { timeout: hardTimeOut });
Expand All @@ -42,6 +42,8 @@ const sitemapHandler = async (url) => {
} catch (error) {
if (error.code === 'ECONNABORTED') {
return { error: `Request timed-out after ${hardTimeOut}ms` };
} else if (error.response && error.response.status === 404) {
return { notApplicable: 'No sitemap.xml found on this domain' };
} else {
return { error: error.message };
}
Expand Down
2 changes: 1 addition & 1 deletion api/social-tags.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import axios from 'axios';
import cheerio from 'cheerio';
import * as cheerio from 'cheerio';
import middleware from './_common/middleware.js';

const socialTagsHandler = async (url) => {
Expand Down
11 changes: 10 additions & 1 deletion api/tech-stack.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@ import Wappalyzer from 'wappalyzer';
import middleware from './_common/middleware.js';

const techStackHandler = async (url) => {
const options = {};
const options = {
browsers: ['chromium'],
puppeteerArgs: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--single-process',
],
executablePath: process.env.CHROME_PATH || '/usr/bin/chromium',
};

const wappalyzer = new Wappalyzer(options);

Expand Down
2 changes: 2 additions & 0 deletions api/txt-records.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ const txtRecordHandler = async (url, event, context) => {
} catch (error) {
if (error.code === 'ERR_INVALID_URL') {
throw new Error(`Invalid URL ${error}`);
} else if (error.code === 'ENODATA' || error.code === 'ENOTFOUND') {
return { notApplicable: 'No TXT records configured for this domain' };
} else {
throw error;
}
Expand Down
12 changes: 10 additions & 2 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const unwrapEnvVar = (varName, fallbackValue) => {
return classicEnvVar || viteEnvVar || fallbackValue;
}

const toBoolean = (val) => (val ? ['true', '1', 'yes', 'on'].includes(String(val).toLowerCase().trim()) : false);

// Determine the deploy target (vercel, netlify, cloudflare, node)
const deployTarget = unwrapEnvVar('PLATFORM', 'node').toLowerCase();

Expand All @@ -32,10 +34,16 @@ const site = unwrapEnvVar('SITE_URL', 'https://web-check.xyz');
const base = unwrapEnvVar('BASE_URL', '/');

// Should run the app in boss-mode (requires extra configuration)
const isBossServer = unwrapEnvVar('BOSS_SERVER', false);
const isBossServer = toBoolean(unwrapEnvVar('BOSS_SERVER', false));

// Initialize Astro integrations
const integrations = [svelte(), react(), partytown(), sitemap()];
// Only include sitemap for boss-mode (production site), not for self-hosted instances
const integrations = [
svelte(),
react(),
partytown(),
...(isBossServer ? [sitemap()] : [])
];

// Set the appropriate adapter, based on the deploy target
function getAdapter(target) {
Expand Down
1 change: 1 addition & 0 deletions netlify.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
[build.environment]
PLATFORM = "netlify"
PUBLIC_API_ENDPOINT = "/.netlify/functions"
BOSS_SERVER = "false"
# Build configuration env vars (uncomment if you want to conigure these)
# CI="false" # Set CI to false, to prevent warnings from exiting the build
# CHROME_PATH='/usr/bin/chromium' # Path to Chromium binary
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"typescript": "^5.4.5",
"unzipper": "^0.11.5",
"url-parse": "^1.5.10",
"uuid": "^9.0.1",
"wappalyzer": "^6.10.65",
"xml2js": "^0.6.2"
},
Expand Down
33 changes: 27 additions & 6 deletions src/web-check-live/components/misc/ProgressBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import colors from 'web-check-live/styles/colors';
import Card from 'web-check-live/components/Form/Card';
import Heading from 'web-check-live/components/Form/Heading';
import { useState, useEffect, type ReactNode } from 'react';
import StatusIconLegend from 'web-check-live/components/misc/StatusIconLegend';


const LoadCard = styled(Card)`
Expand Down Expand Up @@ -173,7 +174,7 @@ pre {
}
`;

export type LoadingState = 'success' | 'loading' | 'skipped' | 'error' | 'timed-out';
export type LoadingState = 'success' | 'loading' | 'skipped' | 'error' | 'timed-out' | 'N/A';

export interface LoadingJob {
name: string,
Expand Down Expand Up @@ -235,6 +236,8 @@ const getStatusEmoji = (state: LoadingState): string => {
switch (state) {
case 'success':
return '✅';
case 'N/A':
return '🔘';
case 'loading':
return '🔄';
case 'error':
Expand All @@ -250,12 +253,22 @@ const getStatusEmoji = (state: LoadingState): string => {

const JobListItem: React.FC<JobListItemProps> = ({ job, showJobDocs, showErrorModal, barColors }) => {
const { name, state, timeTaken, retry, error } = job;
const actionButton = retry && state !== 'success' && state !== 'loading' ?

// Don't show retry button for success and loading states
const showRetry = retry && !['success', 'loading'].includes(state);
const actionButton = showRetry ?
<FailedJobActionButton onClick={retry}>↻ Retry</FailedJobActionButton> : null;

const showModalButton = error && ['error', 'timed-out', 'skipped'].includes(state) &&
<FailedJobActionButton onClick={() => showErrorModal(name, state, timeTaken, error, state === 'skipped')}>
{state === 'timed-out' ? '■ Show Timeout Reason' : '■ Show Error'}
// Determine info button text based on state
const getInfoButtonText = () => {
if (state === 'skipped' || state === 'N/A') return '■ Show Info';
if (state === 'timed-out') return '■ Show Timeout Reason';
return '■ Show Error';
};

const showModalButton = error && ['error', 'timed-out', 'skipped', 'N/A'].includes(state) &&
<FailedJobActionButton onClick={() => showErrorModal(name, state, timeTaken, error, ['skipped', 'N/A'].includes(state))}>
{getInfoButtonText()}
</FailedJobActionButton>;

return (
Expand Down Expand Up @@ -284,6 +297,7 @@ export const calculateLoadingStatePercentages = (loadingJobs: LoadingJob[]): Rec
// Initialize count object
const stateCount: Record<LoadingState, number> = {
'success': 0,
'N/A': 0,
'loading': 0,
'timed-out': 0,
'error': 0,
Expand All @@ -298,6 +312,7 @@ export const calculateLoadingStatePercentages = (loadingJobs: LoadingJob[]): Rec
// Convert counts to percentages
const statePercentage: Record<LoadingState, number> = {
'success': (stateCount['success'] / totalJobs) * 100,
'N/A': (stateCount['N/A'] / totalJobs) * 100,
'loading': (stateCount['loading'] / totalJobs) * 100,
'timed-out': (stateCount['timed-out'] / totalJobs) * 100,
'error': (stateCount['error'] / totalJobs) * 100,
Expand Down Expand Up @@ -345,11 +360,13 @@ const SummaryText = (props: { state: LoadingJob[], count: number }): JSX.Element
let loadingTasksCount = props.state.filter((val: LoadingJob) => val.state === 'loading').length;
let skippedTasksCount = props.state.filter((val: LoadingJob) => val.state === 'skipped').length;
let successTasksCount = props.state.filter((val: LoadingJob) => val.state === 'success').length;
let notApplicableCount = props.state.filter((val: LoadingJob) => val.state === 'N/A').length;

const jobz = (jobCount: number) => `${jobCount} ${jobCount === 1 ? 'job' : 'jobs'}`;

const skippedInfo = skippedTasksCount > 0 ? (<span className="skipped">{jobz(skippedTasksCount)} skipped </span>) : null;
const successInfo = successTasksCount > 0 ? (<span className="success">{jobz(successTasksCount)} successful </span>) : null;
const naInfo = notApplicableCount > 0 ? (<span style={{color: colors.neutral}}>{jobz(notApplicableCount)} N/A </span>) : null;
const failedInfo = failedTasksCount > 0 ? (<span className="error">{jobz(failedTasksCount)} failed </span>) : null;

if (loadingTasksCount > 0) {
Expand All @@ -364,7 +381,8 @@ const SummaryText = (props: { state: LoadingJob[], count: number }): JSX.Element
if (failedTasksCount === 0) {
return (
<SummaryContainer className="success-info">
<b>{successTasksCount} Jobs Completed Successfully</b>
<b>{successTasksCount + notApplicableCount} Jobs Completed</b>
{naInfo}
{skippedInfo}
</SummaryContainer>
);
Expand All @@ -373,6 +391,7 @@ const SummaryText = (props: { state: LoadingJob[], count: number }): JSX.Element
return (
<SummaryContainer className="error-info">
{successInfo}
{naInfo}
{skippedInfo}
{failedInfo}
</SummaryContainer>
Expand All @@ -398,6 +417,7 @@ const ProgressLoader = (props: { loadStatus: LoadingJob[], showModal: (err: Reac

const barColors: Record<LoadingState | string, [string, string]> = {
'success': isDone ? makeBarColor(colors.primary) : makeBarColor(colors.success),
'N/A': makeBarColor(colors.neutral),
'loading': makeBarColor(colors.info),
'error': makeBarColor(colors.danger),
'timed-out': makeBarColor(colors.warning),
Expand Down Expand Up @@ -449,6 +469,7 @@ const ProgressLoader = (props: { loadStatus: LoadingJob[], showModal: (err: Reac
<JobListItem key={job.name} job={job} showJobDocs={props.showJobDocs} showErrorModal={showErrorModal} barColors={barColors} />
))}
</ul>
<StatusIconLegend />
{ loadStatus.filter((val: LoadingJob) => val.state === 'error').length > 0 &&
<p className="error">
<b>Check the browser console for logs and more info</b><br />
Expand Down
Loading