mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
feat(statuspage): added statuspage, updated list of tools in footer, renamed routes (#2139)
* feat(statuspage): added statuspage, updated list of tools in footer, renamed routes * ack PR comments
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
const DEFAULT_STARS = '15.4k'
|
||||
const DEFAULT_STARS = '18.6k'
|
||||
|
||||
const logger = createLogger('GitHubStars')
|
||||
|
||||
export async function getFormattedGitHubStars(): Promise<string> {
|
||||
try {
|
||||
const response = await fetch('/api/github-stars', {
|
||||
const response = await fetch('/api/stars', {
|
||||
headers: {
|
||||
'Cache-Control': 'max-age=3600', // Cache for 1 hour
|
||||
},
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { HIPAABadgeIcon } from '@/components/icons'
|
||||
|
||||
export default function ComplianceBadges() {
|
||||
return (
|
||||
<div className='mt-[6px] flex items-center gap-[12px]'>
|
||||
{/* SOC2 badge */}
|
||||
<Link href='https://trust.delve.co/sim-studio' target='_blank' rel='noopener noreferrer'>
|
||||
<Image
|
||||
src='/footer/soc2.png'
|
||||
alt='SOC2 Compliant'
|
||||
width={54}
|
||||
height={54}
|
||||
className='object-contain'
|
||||
loading='lazy'
|
||||
quality={75}
|
||||
/>
|
||||
</Link>
|
||||
{/* HIPAA badge */}
|
||||
<Link href='https://trust.delve.co/sim-studio' target='_blank' rel='noopener noreferrer'>
|
||||
<HIPAABadgeIcon className='h-[54px] w-[54px]' />
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import ComplianceBadges from './compliance-badges'
|
||||
import Logo from './logo'
|
||||
import SocialLinks from './social-links'
|
||||
import StatusIndicator from './status-indicator'
|
||||
|
||||
export { ComplianceBadges, Logo, SocialLinks, StatusIndicator }
|
||||
17
apps/sim/app/(landing)/components/footer/components/logo.tsx
Normal file
17
apps/sim/app/(landing)/components/footer/components/logo.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function Logo() {
|
||||
return (
|
||||
<Link href='/' aria-label='Sim home'>
|
||||
<Image
|
||||
src='/logo/b&w/text/b&w.svg'
|
||||
alt='Sim - Workflows for LLMs'
|
||||
width={49.78314}
|
||||
height={24.276}
|
||||
priority
|
||||
quality={90}
|
||||
/>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { DiscordIcon, GithubIcon, LinkedInIcon, xIcon as XIcon } from '@/components/icons'
|
||||
|
||||
export default function SocialLinks() {
|
||||
return (
|
||||
<div className='flex items-center gap-[12px]'>
|
||||
<a
|
||||
href='https://discord.gg/Hr4UWYEcTT'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='flex items-center text-[16px] text-muted-foreground transition-colors hover:text-foreground'
|
||||
aria-label='Discord'
|
||||
>
|
||||
<DiscordIcon className='h-[20px] w-[20px]' aria-hidden='true' />
|
||||
</a>
|
||||
<a
|
||||
href='https://x.com/simdotai'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='flex items-center text-[16px] text-muted-foreground transition-colors hover:text-foreground'
|
||||
aria-label='X (Twitter)'
|
||||
>
|
||||
<XIcon className='h-[18px] w-[18px]' aria-hidden='true' />
|
||||
</a>
|
||||
<a
|
||||
href='https://www.linkedin.com/company/simstudioai/'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='flex items-center text-[16px] text-muted-foreground transition-colors hover:text-foreground'
|
||||
aria-label='LinkedIn'
|
||||
>
|
||||
<LinkedInIcon className='h-[18px] w-[18px]' aria-hidden='true' />
|
||||
</a>
|
||||
<a
|
||||
href='https://github.com/simstudioai/sim'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='flex items-center text-[16px] text-muted-foreground transition-colors hover:text-foreground'
|
||||
aria-label='GitHub'
|
||||
>
|
||||
<GithubIcon className='h-[20px] w-[20px]' aria-hidden='true' />
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { StatusDotIcon } from '@/components/icons'
|
||||
import type { StatusType } from '@/app/api/status/types'
|
||||
import { useStatus } from '@/hooks/queries/status'
|
||||
|
||||
const STATUS_COLORS: Record<StatusType, string> = {
|
||||
operational: 'text-[#10B981] hover:text-[#059669]',
|
||||
degraded: 'text-[#F59E0B] hover:text-[#D97706]',
|
||||
outage: 'text-[#EF4444] hover:text-[#DC2626]',
|
||||
maintenance: 'text-[#3B82F6] hover:text-[#2563EB]',
|
||||
loading: 'text-muted-foreground hover:text-foreground',
|
||||
error: 'text-muted-foreground hover:text-foreground',
|
||||
}
|
||||
|
||||
export default function StatusIndicator() {
|
||||
const { data, isLoading, isError } = useStatus()
|
||||
|
||||
const status = isLoading ? 'loading' : isError ? 'error' : data?.status || 'error'
|
||||
const message = isLoading
|
||||
? 'Checking Status...'
|
||||
: isError
|
||||
? 'Status Unknown'
|
||||
: data?.message || 'Status Unknown'
|
||||
const statusUrl = data?.url || 'https://status.sim.ai'
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={statusUrl}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className={`flex items-center gap-[6px] whitespace-nowrap text-[12px] transition-colors ${STATUS_COLORS[status]}`}
|
||||
aria-label={`System status: ${message}`}
|
||||
>
|
||||
<StatusDotIcon status={status} className='h-[6px] w-[6px]' aria-hidden='true' />
|
||||
<span>{message}</span>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
96
apps/sim/app/(landing)/components/footer/consts.ts
Normal file
96
apps/sim/app/(landing)/components/footer/consts.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
export const FOOTER_BLOCKS = [
|
||||
'Agent',
|
||||
'API',
|
||||
'Condition',
|
||||
'Evaluator',
|
||||
'Function',
|
||||
'Human In The Loop',
|
||||
'Loop',
|
||||
'Parallel',
|
||||
'Response',
|
||||
'Router',
|
||||
'Starter',
|
||||
'Webhook',
|
||||
'Workflow',
|
||||
]
|
||||
|
||||
export const FOOTER_TOOLS = [
|
||||
'Airtable',
|
||||
'Apify',
|
||||
'Apollo',
|
||||
'ArXiv',
|
||||
'Browser Use',
|
||||
'Calendly',
|
||||
'Clay',
|
||||
'Confluence',
|
||||
'Discord',
|
||||
'ElevenLabs',
|
||||
'Exa',
|
||||
'Firecrawl',
|
||||
'GitHub',
|
||||
'Gmail',
|
||||
'Google Drive',
|
||||
'Guardrails',
|
||||
'HubSpot',
|
||||
'HuggingFace',
|
||||
'Hunter',
|
||||
'Incidentio',
|
||||
'Intercom',
|
||||
'Jina',
|
||||
'Jira',
|
||||
'Knowledge',
|
||||
'Linear',
|
||||
'LinkUp',
|
||||
'LinkedIn',
|
||||
'Mailchimp',
|
||||
'Mailgun',
|
||||
'MCP',
|
||||
'Mem0',
|
||||
'Microsoft Excel',
|
||||
'Microsoft Planner',
|
||||
'Microsoft Teams',
|
||||
'Mistral Parse',
|
||||
'MongoDB',
|
||||
'MySQL',
|
||||
'Neo4j',
|
||||
'Notion',
|
||||
'OneDrive',
|
||||
'OpenAI',
|
||||
'Outlook',
|
||||
'Parallel AI',
|
||||
'Perplexity',
|
||||
'Pinecone',
|
||||
'Pipedrive',
|
||||
'PostHog',
|
||||
'PostgreSQL',
|
||||
'Pylon',
|
||||
'Qdrant',
|
||||
'Reddit',
|
||||
'Resend',
|
||||
'S3',
|
||||
'Salesforce',
|
||||
'SendGrid',
|
||||
'Serper',
|
||||
'SharePoint',
|
||||
'Slack',
|
||||
'Smtp',
|
||||
'Stagehand',
|
||||
'Stripe',
|
||||
'Supabase',
|
||||
'Tavily',
|
||||
'Telegram',
|
||||
'Translate',
|
||||
'Trello',
|
||||
'Twilio',
|
||||
'Typeform',
|
||||
'Vision',
|
||||
'Wait',
|
||||
'Wealthbox',
|
||||
'Webflow',
|
||||
'WhatsApp',
|
||||
'Wikipedia',
|
||||
'X',
|
||||
'YouTube',
|
||||
'Zendesk',
|
||||
'Zep',
|
||||
]
|
||||
@@ -1,97 +1,12 @@
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
DiscordIcon,
|
||||
GithubIcon,
|
||||
HIPAABadgeIcon,
|
||||
LinkedInIcon,
|
||||
xIcon as XIcon,
|
||||
} from '@/components/icons'
|
||||
import { inter } from '@/app/_styles/fonts/inter/inter'
|
||||
|
||||
const blocks = [
|
||||
'Agent',
|
||||
'API',
|
||||
'Condition',
|
||||
'Evaluator',
|
||||
'Function',
|
||||
'Loop',
|
||||
'Parallel',
|
||||
'Response',
|
||||
'Router',
|
||||
'Starter',
|
||||
'Webhook',
|
||||
'Workflow',
|
||||
]
|
||||
|
||||
const tools = [
|
||||
'Airtable',
|
||||
'ArXiv',
|
||||
'Browser Use',
|
||||
'Clay',
|
||||
'Confluence',
|
||||
'Discord',
|
||||
'ElevenLabs',
|
||||
'Exa',
|
||||
'File',
|
||||
'Firecrawl',
|
||||
'Generic Webhook',
|
||||
'GitHub',
|
||||
'Gmail',
|
||||
'Google Calendar',
|
||||
'Google Docs',
|
||||
'Google Drive',
|
||||
'Google Vault',
|
||||
'Google Search',
|
||||
'Google Sheets',
|
||||
'HuggingFace',
|
||||
'Hunter',
|
||||
'Image Generator',
|
||||
'Jina',
|
||||
'Jira',
|
||||
'Knowledge',
|
||||
'Linear',
|
||||
'LinkUp',
|
||||
'Mem0',
|
||||
'Memory',
|
||||
'Microsoft Excel',
|
||||
'Microsoft Planner',
|
||||
'Microsoft Teams',
|
||||
'Mistral Parse',
|
||||
'MySQL',
|
||||
'Notion',
|
||||
'OneDrive',
|
||||
'OpenAI',
|
||||
'Outlook',
|
||||
'Parallel AI',
|
||||
'Perplexity',
|
||||
'Pinecone',
|
||||
'PostgreSQL',
|
||||
'Qdrant',
|
||||
'Reddit',
|
||||
'S3',
|
||||
'Schedule',
|
||||
'Serper',
|
||||
'SharePoint',
|
||||
'Slack',
|
||||
'Stagehand',
|
||||
'Stagehand Agent',
|
||||
'Supabase',
|
||||
'Tavily',
|
||||
'Telegram',
|
||||
'Thinking',
|
||||
'Translate',
|
||||
'Twilio SMS',
|
||||
'Typeform',
|
||||
'Vision',
|
||||
'Wealthbox',
|
||||
'Webhook',
|
||||
'WhatsApp',
|
||||
'Wikipedia',
|
||||
'X',
|
||||
'YouTube',
|
||||
'Zep',
|
||||
]
|
||||
import {
|
||||
ComplianceBadges,
|
||||
Logo,
|
||||
SocialLinks,
|
||||
StatusIndicator,
|
||||
} from '@/app/(landing)/components/footer/components'
|
||||
import { FOOTER_BLOCKS, FOOTER_TOOLS } from '@/app/(landing)/components/footer/consts'
|
||||
|
||||
interface FooterProps {
|
||||
fullWidth?: boolean
|
||||
@@ -110,84 +25,10 @@ export default function Footer({ fullWidth = false }: FooterProps) {
|
||||
<div className={`flex gap-[80px] ${fullWidth ? 'justify-center' : ''}`}>
|
||||
{/* Logo and social links */}
|
||||
<div className='flex flex-col gap-[24px]'>
|
||||
<Link href='/' aria-label='Sim home'>
|
||||
<Image
|
||||
src='/logo/b&w/text/b&w.svg'
|
||||
alt='Sim - Workflows for LLMs'
|
||||
width={49.78314}
|
||||
height={24.276}
|
||||
priority
|
||||
quality={90}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
{/* Social links */}
|
||||
<div className='flex items-center gap-[12px]'>
|
||||
<a
|
||||
href='https://discord.gg/Hr4UWYEcTT'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='flex items-center text-[16px] text-muted-foreground transition-colors hover:text-foreground'
|
||||
aria-label='Discord'
|
||||
>
|
||||
<DiscordIcon className='h-[20px] w-[20px]' aria-hidden='true' />
|
||||
</a>
|
||||
<a
|
||||
href='https://x.com/simdotai'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='flex items-center text-[16px] text-muted-foreground transition-colors hover:text-foreground'
|
||||
aria-label='X (Twitter)'
|
||||
>
|
||||
<XIcon className='h-[18px] w-[18px]' aria-hidden='true' />
|
||||
</a>
|
||||
<a
|
||||
href='https://www.linkedin.com/company/simstudioai/'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='flex items-center text-[16px] text-muted-foreground transition-colors hover:text-foreground'
|
||||
aria-label='LinkedIn'
|
||||
>
|
||||
<LinkedInIcon className='h-[18px] w-[18px]' aria-hidden='true' />
|
||||
</a>
|
||||
<a
|
||||
href='https://github.com/simstudioai/sim'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='flex items-center text-[16px] text-muted-foreground transition-colors hover:text-foreground'
|
||||
aria-label='GitHub'
|
||||
>
|
||||
<GithubIcon className='h-[20px] w-[20px]' aria-hidden='true' />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Compliance badges */}
|
||||
<div className='mt-[6px] flex items-center gap-[12px]'>
|
||||
{/* SOC2 badge */}
|
||||
<Link
|
||||
href='https://trust.delve.co/sim-studio'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<Image
|
||||
src='/footer/soc2.png'
|
||||
alt='SOC2 Compliant'
|
||||
width={54}
|
||||
height={54}
|
||||
className='object-contain'
|
||||
loading='lazy'
|
||||
quality={75}
|
||||
/>
|
||||
</Link>
|
||||
{/* HIPAA badge placeholder - add when available */}
|
||||
<Link
|
||||
href='https://trust.delve.co/sim-studio'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<HIPAABadgeIcon className='h-[54px] w-[54px]' />
|
||||
</Link>
|
||||
</div>
|
||||
<Logo />
|
||||
<SocialLinks />
|
||||
<ComplianceBadges />
|
||||
<StatusIndicator />
|
||||
</div>
|
||||
|
||||
{/* Links section */}
|
||||
@@ -228,6 +69,14 @@ export default function Footer({ fullWidth = false }: FooterProps) {
|
||||
>
|
||||
Changelog
|
||||
</Link>
|
||||
<Link
|
||||
href='https://status.sim.ai'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-[14px] text-muted-foreground transition-colors hover:text-foreground'
|
||||
>
|
||||
Status
|
||||
</Link>
|
||||
<Link
|
||||
href='/careers'
|
||||
className='text-[14px] text-muted-foreground transition-colors hover:text-foreground'
|
||||
@@ -257,7 +106,7 @@ export default function Footer({ fullWidth = false }: FooterProps) {
|
||||
<div className='hidden sm:block'>
|
||||
<h2 className='mb-[16px] font-medium text-[14px] text-foreground'>Blocks</h2>
|
||||
<div className='flex flex-col gap-[12px]'>
|
||||
{blocks.map((block) => (
|
||||
{FOOTER_BLOCKS.map((block) => (
|
||||
<Link
|
||||
key={block}
|
||||
href={`https://docs.sim.ai/blocks/${block.toLowerCase().replace(' ', '-')}`}
|
||||
@@ -277,7 +126,7 @@ export default function Footer({ fullWidth = false }: FooterProps) {
|
||||
<div className='flex gap-[80px]'>
|
||||
{/* First column */}
|
||||
<div className='flex flex-col gap-[12px]'>
|
||||
{tools.slice(0, Math.ceil(tools.length / 4)).map((tool) => (
|
||||
{FOOTER_TOOLS.slice(0, Math.ceil(FOOTER_TOOLS.length / 4)).map((tool) => (
|
||||
<Link
|
||||
key={tool}
|
||||
href={`https://docs.sim.ai/tools/${tool.toLowerCase().replace(/\s+/g, '_')}`}
|
||||
@@ -291,39 +140,41 @@ export default function Footer({ fullWidth = false }: FooterProps) {
|
||||
</div>
|
||||
{/* Second column */}
|
||||
<div className='flex flex-col gap-[12px]'>
|
||||
{tools
|
||||
.slice(Math.ceil(tools.length / 4), Math.ceil((tools.length * 2) / 4))
|
||||
.map((tool) => (
|
||||
<Link
|
||||
key={tool}
|
||||
href={`https://docs.sim.ai/tools/${tool.toLowerCase().replace(/\s+/g, '_')}`}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='whitespace-nowrap text-[14px] text-muted-foreground transition-colors hover:text-foreground'
|
||||
>
|
||||
{tool}
|
||||
</Link>
|
||||
))}
|
||||
{FOOTER_TOOLS.slice(
|
||||
Math.ceil(FOOTER_TOOLS.length / 4),
|
||||
Math.ceil((FOOTER_TOOLS.length * 2) / 4)
|
||||
).map((tool) => (
|
||||
<Link
|
||||
key={tool}
|
||||
href={`https://docs.sim.ai/tools/${tool.toLowerCase().replace(/\s+/g, '_')}`}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='whitespace-nowrap text-[14px] text-muted-foreground transition-colors hover:text-foreground'
|
||||
>
|
||||
{tool}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
{/* Third column */}
|
||||
<div className='flex flex-col gap-[12px]'>
|
||||
{tools
|
||||
.slice(Math.ceil((tools.length * 2) / 4), Math.ceil((tools.length * 3) / 4))
|
||||
.map((tool) => (
|
||||
<Link
|
||||
key={tool}
|
||||
href={`https://docs.sim.ai/tools/${tool.toLowerCase().replace(/\s+/g, '_')}`}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='whitespace-nowrap text-[14px] text-muted-foreground transition-colors hover:text-foreground'
|
||||
>
|
||||
{tool}
|
||||
</Link>
|
||||
))}
|
||||
{FOOTER_TOOLS.slice(
|
||||
Math.ceil((FOOTER_TOOLS.length * 2) / 4),
|
||||
Math.ceil((FOOTER_TOOLS.length * 3) / 4)
|
||||
).map((tool) => (
|
||||
<Link
|
||||
key={tool}
|
||||
href={`https://docs.sim.ai/tools/${tool.toLowerCase().replace(/\s+/g, '_')}`}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='whitespace-nowrap text-[14px] text-muted-foreground transition-colors hover:text-foreground'
|
||||
>
|
||||
{tool}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
{/* Fourth column */}
|
||||
<div className='flex flex-col gap-[12px]'>
|
||||
{tools.slice(Math.ceil((tools.length * 3) / 4)).map((tool) => (
|
||||
{FOOTER_TOOLS.slice(Math.ceil((FOOTER_TOOLS.length * 3) / 4)).map((tool) => (
|
||||
<Link
|
||||
key={tool}
|
||||
href={`https://docs.sim.ai/tools/${tool.toLowerCase().replace(/\s+/g, '_')}`}
|
||||
|
||||
@@ -20,7 +20,7 @@ interface NavProps {
|
||||
}
|
||||
|
||||
export default function Nav({ hideAuthButtons = false, variant = 'landing' }: NavProps = {}) {
|
||||
const [githubStars, setGithubStars] = useState('18.5k')
|
||||
const [githubStars, setGithubStars] = useState('18.6k')
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const [isLoginHovered, setIsLoginHovered] = useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
97
apps/sim/app/api/status/route.ts
Normal file
97
apps/sim/app/api/status/route.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type { IncidentIOWidgetResponse, StatusResponse, StatusType } from '@/app/api/status/types'
|
||||
|
||||
const logger = createLogger('StatusAPI')
|
||||
|
||||
let cachedResponse: { data: StatusResponse; timestamp: number } | null = null
|
||||
const CACHE_TTL = 2 * 60 * 1000
|
||||
|
||||
function determineStatus(data: IncidentIOWidgetResponse): {
|
||||
status: StatusType
|
||||
message: string
|
||||
} {
|
||||
if (data.ongoing_incidents && data.ongoing_incidents.length > 0) {
|
||||
const worstImpact = data.ongoing_incidents[0].current_worst_impact
|
||||
|
||||
if (worstImpact === 'full_outage') {
|
||||
return { status: 'outage', message: 'Service Disruption' }
|
||||
}
|
||||
if (worstImpact === 'partial_outage') {
|
||||
return { status: 'degraded', message: 'Experiencing Issues' }
|
||||
}
|
||||
return { status: 'degraded', message: 'Experiencing Issues' }
|
||||
}
|
||||
|
||||
if (data.in_progress_maintenances && data.in_progress_maintenances.length > 0) {
|
||||
return { status: 'maintenance', message: 'Under Maintenance' }
|
||||
}
|
||||
|
||||
return { status: 'operational', message: 'All Systems Operational' }
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const now = Date.now()
|
||||
|
||||
if (cachedResponse && now - cachedResponse.timestamp < CACHE_TTL) {
|
||||
return NextResponse.json(cachedResponse.data, {
|
||||
headers: {
|
||||
'Cache-Control': 'public, max-age=60, s-maxage=60',
|
||||
'X-Cache': 'HIT',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const response = await fetch('https://status.sim.ai/api/v1/summary', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
signal: AbortSignal.timeout(5000),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`incident.io API returned ${response.status}`)
|
||||
}
|
||||
|
||||
const data: IncidentIOWidgetResponse = await response.json()
|
||||
|
||||
const { status, message } = determineStatus(data)
|
||||
|
||||
const statusResponse: StatusResponse = {
|
||||
status,
|
||||
message,
|
||||
url: data.page_url || 'https://status.sim.ai',
|
||||
lastUpdated: new Date().toISOString(),
|
||||
}
|
||||
|
||||
cachedResponse = {
|
||||
data: statusResponse,
|
||||
timestamp: now,
|
||||
}
|
||||
|
||||
return NextResponse.json(statusResponse, {
|
||||
headers: {
|
||||
'Cache-Control': 'public, max-age=60, s-maxage=60',
|
||||
'X-Cache': 'MISS',
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error fetching status from incident.io:', error)
|
||||
|
||||
const errorResponse: StatusResponse = {
|
||||
status: 'error',
|
||||
message: 'Status Unknown',
|
||||
url: 'https://status.sim.ai',
|
||||
lastUpdated: new Date().toISOString(),
|
||||
}
|
||||
|
||||
return NextResponse.json(errorResponse, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Cache-Control': 'public, max-age=30, s-maxage=30',
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
48
apps/sim/app/api/status/types.ts
Normal file
48
apps/sim/app/api/status/types.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
export interface IncidentIOComponent {
|
||||
id: string
|
||||
name: string
|
||||
group_name?: string
|
||||
current_status: 'operational' | 'degraded_performance' | 'partial_outage' | 'full_outage'
|
||||
}
|
||||
|
||||
export interface IncidentIOIncident {
|
||||
id: string
|
||||
name: string
|
||||
status: 'investigating' | 'identified' | 'monitoring'
|
||||
url: string
|
||||
last_update_at: string
|
||||
last_update_message: string
|
||||
current_worst_impact: 'degraded_performance' | 'partial_outage' | 'full_outage'
|
||||
affected_components: IncidentIOComponent[]
|
||||
}
|
||||
|
||||
export interface IncidentIOMaintenance {
|
||||
id: string
|
||||
name: string
|
||||
status: 'maintenance_scheduled' | 'maintenance_in_progress'
|
||||
url: string
|
||||
last_update_at: string
|
||||
last_update_message: string
|
||||
affected_components: IncidentIOComponent[]
|
||||
started_at?: string
|
||||
scheduled_end_at?: string
|
||||
starts_at?: string
|
||||
ends_at?: string
|
||||
}
|
||||
|
||||
export interface IncidentIOWidgetResponse {
|
||||
page_title: string
|
||||
page_url: string
|
||||
ongoing_incidents: IncidentIOIncident[]
|
||||
in_progress_maintenances: IncidentIOMaintenance[]
|
||||
scheduled_maintenances: IncidentIOMaintenance[]
|
||||
}
|
||||
|
||||
export type StatusType = 'operational' | 'degraded' | 'outage' | 'maintenance' | 'loading' | 'error'
|
||||
|
||||
export interface StatusResponse {
|
||||
status: StatusType
|
||||
message: string
|
||||
url: string
|
||||
lastUpdated: string
|
||||
}
|
||||
@@ -164,7 +164,7 @@ export function useWand({
|
||||
// Keep track of the current prompt for history
|
||||
const currentPrompt = prompt
|
||||
|
||||
const response = await fetch('/api/wand-generate', {
|
||||
const response = await fetch('/api/wand', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -836,17 +836,18 @@ export function DiscordIcon(props: SVGProps<SVGSVGElement>) {
|
||||
|
||||
export function LinkedInIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} height='72' viewBox='0 0 72 72' width='72' xmlns='http://www.w3.org/2000/svg'>
|
||||
<g fill='none' fillRule='evenodd'>
|
||||
<path
|
||||
d='M8,72 L64,72 C68.418278,72 72,68.418278 72,64 L72,8 C72,3.581722 68.418278,-8.11624501e-16 64,0 L8,0 C3.581722,8.11624501e-16 -5.41083001e-16,3.581722 0,8 L0,64 C5.41083001e-16,68.418278 3.581722,72 8,72 Z'
|
||||
fill='#0072B1'
|
||||
/>
|
||||
<path
|
||||
d='M62,62 L51.315625,62 L51.315625,43.8021149 C51.315625,38.8127542 49.4197917,36.0245323 45.4707031,36.0245323 C41.1746094,36.0245323 38.9300781,38.9261103 38.9300781,43.8021149 L38.9300781,62 L28.6333333,62 L28.6333333,27.3333333 L38.9300781,27.3333333 L38.9300781,32.0029283 C38.9300781,32.0029283 42.0260417,26.2742151 49.3825521,26.2742151 C56.7356771,26.2742151 62,30.7644705 62,40.051212 L62,62 Z M16.349349,22.7940133 C12.8420573,22.7940133 10,19.9296567 10,16.3970067 C10,12.8643566 12.8420573,10 16.349349,10 C19.8566406,10 22.6970052,12.8643566 22.6970052,16.3970067 C22.6970052,19.9296567 19.8566406,22.7940133 16.349349,22.7940133 Z M11.0325521,62 L21.769401,62 L21.769401,27.3333333 L11.0325521,27.3333333 L11.0325521,62 Z'
|
||||
fill='#FFF'
|
||||
/>
|
||||
</g>
|
||||
<svg
|
||||
{...props}
|
||||
fill='currentColor'
|
||||
height='72'
|
||||
viewBox='0 0 72 72'
|
||||
width='72'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<path
|
||||
d='M62,62 L51.315625,62 L51.315625,43.8021149 C51.315625,38.8127542 49.4197917,36.0245323 45.4707031,36.0245323 C41.1746094,36.0245323 38.9300781,38.9261103 38.9300781,43.8021149 L38.9300781,62 L28.6333333,62 L28.6333333,27.3333333 L38.9300781,27.3333333 L38.9300781,32.0029283 C38.9300781,32.0029283 42.0260417,26.2742151 49.3825521,26.2742151 C56.7356771,26.2742151 62,30.7644705 62,40.051212 L62,62 Z M16.349349,22.7940133 C12.8420573,22.7940133 10,19.9296567 10,16.3970067 C10,12.8643566 12.8420573,10 16.349349,10 C19.8566406,10 22.6970052,12.8643566 22.6970052,16.3970067 C22.6970052,19.9296567 19.8566406,22.7940133 16.349349,22.7940133 Z M11.0325521,62 L21.769401,62 L21.769401,27.3333333 L11.0325521,27.3333333 L11.0325521,62 Z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -3833,3 +3834,32 @@ export function ApifyIcon(props: SVGProps<SVGSVGElement>) {
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
interface StatusDotIconProps extends SVGProps<SVGSVGElement> {
|
||||
status: 'operational' | 'degraded' | 'outage' | 'maintenance' | 'loading' | 'error'
|
||||
}
|
||||
|
||||
export function StatusDotIcon({ status, className, ...props }: StatusDotIconProps) {
|
||||
const colors = {
|
||||
operational: '#10B981',
|
||||
degraded: '#F59E0B',
|
||||
outage: '#EF4444',
|
||||
maintenance: '#3B82F6',
|
||||
loading: '#9CA3AF',
|
||||
error: '#9CA3AF',
|
||||
}
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width={6}
|
||||
height={6}
|
||||
viewBox='0 0 6 6'
|
||||
fill='none'
|
||||
className={className}
|
||||
{...props}
|
||||
>
|
||||
<circle cx={3} cy={3} r={3} fill={colors[status]} />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
42
apps/sim/hooks/queries/status.ts
Normal file
42
apps/sim/hooks/queries/status.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import type { StatusResponse } from '@/app/api/status/types'
|
||||
|
||||
/**
|
||||
* Query key factories for status-related queries
|
||||
* This ensures consistent cache invalidation across the app
|
||||
*/
|
||||
export const statusKeys = {
|
||||
all: ['status'] as const,
|
||||
current: () => [...statusKeys.all, 'current'] as const,
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch current system status from the API
|
||||
* The API proxies incident.io and caches for 2 minutes server-side
|
||||
*/
|
||||
async function fetchStatus(): Promise<StatusResponse> {
|
||||
const response = await fetch('/api/status')
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch status')
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch current system status
|
||||
* - Polls every 60 seconds to keep status up-to-date
|
||||
* - Refetches when user returns to tab for immediate updates
|
||||
* - Caches for 1 minute to reduce unnecessary requests
|
||||
*/
|
||||
export function useStatus() {
|
||||
return useQuery({
|
||||
queryKey: statusKeys.current(),
|
||||
queryFn: fetchStatus,
|
||||
staleTime: 60 * 1000, // 1 minute
|
||||
refetchInterval: 60 * 1000, // Poll every 60 seconds
|
||||
refetchOnWindowFocus: true, // Refetch when user returns to tab
|
||||
retry: 2,
|
||||
})
|
||||
}
|
||||
@@ -31,11 +31,11 @@ export function getTriggerOptions(): TriggerOption[] {
|
||||
const providerMap = new Map<string, TriggerOption>()
|
||||
|
||||
const coreTypes: TriggerOption[] = [
|
||||
{ value: 'manual', label: 'Manual', color: '#6b7280' }, // gray-500
|
||||
{ value: 'api', label: 'API', color: '#3b82f6' }, // blue-500
|
||||
{ value: 'schedule', label: 'Schedule', color: '#10b981' }, // green-500
|
||||
{ value: 'chat', label: 'Chat', color: '#8b5cf6' }, // purple-500
|
||||
{ value: 'webhook', label: 'Webhook', color: '#f97316' }, // orange-500 (for backward compatibility)
|
||||
{ value: 'manual', label: 'Manual', color: '#6b7280' },
|
||||
{ value: 'api', label: 'API', color: '#3b82f6' },
|
||||
{ value: 'schedule', label: 'Schedule', color: '#10b981' },
|
||||
{ value: 'chat', label: 'Chat', color: '#8b5cf6' },
|
||||
{ value: 'webhook', label: 'Webhook', color: '#f97316' },
|
||||
]
|
||||
|
||||
for (const trigger of triggers) {
|
||||
|
||||
@@ -82,13 +82,12 @@ export const FILTER_DEFINITIONS: FilterDefinition[] = [
|
||||
},
|
||||
]
|
||||
|
||||
// Core trigger types that are always available
|
||||
const CORE_TRIGGERS: TriggerData[] = [
|
||||
{ value: 'api', label: 'API', color: '#3b82f6' }, // blue-500
|
||||
{ value: 'manual', label: 'Manual', color: '#6b7280' }, // gray-500
|
||||
{ value: 'webhook', label: 'Webhook', color: '#f97316' }, // orange-500
|
||||
{ value: 'chat', label: 'Chat', color: '#8b5cf6' }, // purple-500
|
||||
{ value: 'schedule', label: 'Schedule', color: '#10b981' }, // green-500
|
||||
{ value: 'api', label: 'API', color: '#3b82f6' },
|
||||
{ value: 'manual', label: 'Manual', color: '#6b7280' },
|
||||
{ value: 'webhook', label: 'Webhook', color: '#f97316' },
|
||||
{ value: 'chat', label: 'Chat', color: '#8b5cf6' },
|
||||
{ value: 'schedule', label: 'Schedule', color: '#10b981' },
|
||||
]
|
||||
|
||||
export class SearchSuggestions {
|
||||
|
||||
Reference in New Issue
Block a user