Compare commits
1 Commits
release-63
...
release-64
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
459d7c91f7 |
@@ -19,6 +19,7 @@ import {
|
||||
type ServiceVisibility,
|
||||
ServiceSuggestionType,
|
||||
KycLevelClarification,
|
||||
VerificationStepStatus,
|
||||
} from '@prisma/client'
|
||||
import { omit, uniqBy } from 'lodash-es'
|
||||
import { generateUsername } from 'unique-username-generator'
|
||||
@@ -610,6 +611,10 @@ const generateFakeService = (users: User[]) => {
|
||||
const name = faker.helpers.arrayElement(serviceNames)
|
||||
const slug = `${faker.helpers.slugify(name).toLowerCase()}-${faker.string.alphanumeric({ length: 6, casing: 'lower' })}`
|
||||
|
||||
const tosReview = faker.helpers.maybe(() => faker.helpers.arrayElement(tosReviewExamples), {
|
||||
probability: 0.8,
|
||||
})
|
||||
|
||||
return {
|
||||
name,
|
||||
slug,
|
||||
@@ -643,6 +648,19 @@ const generateFakeService = (users: User[]) => {
|
||||
},
|
||||
verificationProofMd:
|
||||
status === 'VERIFICATION_SUCCESS' || status === 'VERIFICATION_FAILED' ? faker.lorem.paragraphs() : null,
|
||||
verificationSteps:
|
||||
(status === 'VERIFICATION_SUCCESS' || status === 'VERIFICATION_FAILED') && faker.datatype.boolean(0.75)
|
||||
? {
|
||||
create: Array.from({ length: faker.number.int({ min: 1, max: 5 }) }, () => ({
|
||||
title: faker.lorem.sentence(),
|
||||
description: faker.lorem.paragraph(),
|
||||
status: faker.helpers.arrayElement(Object.values(VerificationStepStatus)),
|
||||
evidenceMd: faker.lorem.paragraph(),
|
||||
createdAt: faker.date.recent(),
|
||||
updatedAt: faker.date.recent(),
|
||||
})),
|
||||
}
|
||||
: undefined,
|
||||
referral: faker.helpers.arrayElement([
|
||||
`?ref=${faker.string.alphanumeric(6)}`,
|
||||
`/ref/${faker.string.alphanumeric(6)}`,
|
||||
@@ -661,8 +679,10 @@ const generateFakeService = (users: User[]) => {
|
||||
imageUrl: `https://ui-avatars.com/api/?name=${encodeURIComponent(name)}&background=random&format=svg`,
|
||||
listedAt: faker.date.past(),
|
||||
verifiedAt: status === VerificationStatus.VERIFICATION_SUCCESS ? faker.date.past() : null,
|
||||
tosReview: faker.helpers.arrayElement(tosReviewExamples),
|
||||
tosReviewAt: faker.date.past(),
|
||||
tosReview,
|
||||
tosReviewAt: tosReview
|
||||
? faker.date.recent()
|
||||
: faker.helpers.maybe(() => faker.date.recent(), { probability: 0.5 }),
|
||||
userSentiment: faker.helpers.maybe(() => generateFakeUserSentiment(), { probability: 0.8 }),
|
||||
userSentimentAt: faker.date.recent(),
|
||||
internalNotes: faker.helpers.maybe(
|
||||
|
||||
@@ -135,6 +135,17 @@ BEGIN
|
||||
SET "isRecentlyListed" = FALSE
|
||||
WHERE id = service_id;
|
||||
END IF;
|
||||
|
||||
-- Apply penalty if ToS cannot be analyzed
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM "Service"
|
||||
WHERE id = service_id
|
||||
AND "tosReviewAt" IS NOT NULL
|
||||
AND "tosReview" IS NULL
|
||||
) THEN
|
||||
trust_score := trust_score - 3;
|
||||
END IF;
|
||||
|
||||
-- Calculate final trust score (base 100)
|
||||
trust_score := trust_score + verification_factor + attributes_score;
|
||||
|
||||
@@ -41,6 +41,8 @@ export function makeNonDbAttributes(
|
||||
isRecentlyListed: true
|
||||
listedAt: true
|
||||
createdAt: true
|
||||
tosReviewAt: true
|
||||
tosReview: true
|
||||
}
|
||||
}>,
|
||||
{ filter = false }: { filter?: boolean } = {}
|
||||
@@ -156,6 +158,17 @@ export function makeNonDbAttributes(
|
||||
trustPoints: -5,
|
||||
links: [],
|
||||
},
|
||||
{
|
||||
title: "Can't analyse ToS",
|
||||
show: service.tosReviewAt !== null && service.tosReview === null,
|
||||
type: 'WARNING',
|
||||
category: 'TRUST',
|
||||
description:
|
||||
'The Terms of Service page is not analyable by our AI. Possible reasons may be: captchas, client side rendering, DDoS protections, or non-text format.',
|
||||
privacyPoints: 0,
|
||||
trustPoints: -3,
|
||||
links: [],
|
||||
},
|
||||
]
|
||||
|
||||
if (filter) return nonDbAttributes.filter(({ show }) => show)
|
||||
|
||||
@@ -57,7 +57,6 @@ Some users are **verified**, this means that the moderators have confirmed that
|
||||
|
||||
Users can also be **affiliated** with a service if they're related to it, such as being an owner or part of the team. If you own a service and want to get verified, just reach out to us.
|
||||
|
||||
|
||||
## Listings
|
||||
|
||||
### Suggesting a new listing
|
||||
@@ -141,7 +140,7 @@ You can view all available attributes on the [Attributes page](/attributes).
|
||||
Attributes are classified into two main types:
|
||||
|
||||
1. **Privacy Attributes** – Related to data protection and anonymity.
|
||||
2. **Trust Attributes** – Related to reliability and security.
|
||||
1. **Trust Attributes** – Related to reliability and security.
|
||||
|
||||
These categories **directly influence** a service's Privacy and Trust scores, which contribute to its **overall rating**.
|
||||
|
||||
@@ -154,31 +153,32 @@ Scores are calculated **automatically** using clear, fixed rules. We do not chan
|
||||
The privacy score measures how well a service protects user privacy, using a transparent, rules-based approach:
|
||||
|
||||
1. **Base Score:** Every service starts with a neutral score of 50 points.
|
||||
2. **KYC Level:** Adjusts the score based on the level of identity verification required:
|
||||
1. **KYC Level:** Adjusts the score based on the level of identity verification required:
|
||||
- KYC Level 0 (No KYC): **+25 points**
|
||||
- KYC Level 1 (Minimal KYC): **+10 points**
|
||||
- KYC Level 2 (Moderate KYC): **-5 points**
|
||||
- KYC Level 3 (More KYC): **-15 points**
|
||||
- KYC Level 4 (Full mandatory KYC): **-25 points**
|
||||
3. **Onion URL:** **+5 points** if the service offers at least one Onion (Tor) URL.
|
||||
4. **I2P URL:** **+5 points** if the service offers at least one I2P URL.
|
||||
5. **Monero Acceptance:** **+5 points** if the service accepts Monero as a payment method.
|
||||
6. **Privacy Attributes:** The sum of all privacy points from attributes categorized as 'PRIVACY' is added to the score. [See all attributes](/attributes).
|
||||
7. **Final Score Range:** The final score is always kept between 0 and 100.
|
||||
1. **Onion URL:** **+5 points** if the service offers at least one Onion (Tor) URL.
|
||||
1. **I2P URL:** **+5 points** if the service offers at least one I2P URL.
|
||||
1. **Monero Acceptance:** **+5 points** if the service accepts Monero as a payment method.
|
||||
1. **Privacy Attributes:** The sum of all privacy points from attributes categorized as 'PRIVACY' is added to the score. [See all attributes](/attributes).
|
||||
1. **Final Score Range:** The final score is always kept between 0 and 100.
|
||||
|
||||
##### Trust Score
|
||||
|
||||
The trust score represents how reliable and trustworthy a service is, based on objective, transparent criteria.
|
||||
|
||||
1. **Base Score:** Every service begins with a neutral score of 50 points.
|
||||
2. **Verification Status Adjustment:**
|
||||
1. **Verification Status:**
|
||||
- **Verification Success:** +10 points
|
||||
- **Approved:** +5 points
|
||||
- **Community Contributed:** 0 points
|
||||
- **Verification Failed (SCAM):** -50 points
|
||||
3. **Trust Attributes:** The total trust points from all attributes categorized as 'TRUST' are added to the score. [See all attributes](/attributes).
|
||||
4. **Recently Listed Penalty & Flag:** If a service was listed within the last 15 days and its status is `APPROVED`, a penalty of -10 points is applied to the trust score, and the service is flagged as recently listed.
|
||||
5. **Final Score Range:** The final score is always kept between 0 and 100.
|
||||
1. **Recently Listed:** If a service was listed within the last 15 days and its status is `APPROVED`, a penalty of -10 points is applied to the trust score, and the service is flagged as recently listed.
|
||||
1. **Can't Analyze ToS:** If a service's Terms of Service cannot be analyzed by our AI (usually due to captchas, client-side rendering, DDoS protections, or non-text format), a penalty of -3 points is applied to the trust score.
|
||||
1. **Trust Attributes:** The total trust points from all attributes categorized as 'TRUST' are added to the score. [See all attributes](/attributes).
|
||||
1. **Final Score Range:** The final score is always kept between 0 and 100.
|
||||
|
||||
##### Overall Score
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import { head, orderBy, pick, shuffle, sortBy, tail } from 'lodash-es'
|
||||
import AdminOnly from '../../components/AdminOnly.astro'
|
||||
import BadgeSmall from '../../components/BadgeSmall.astro'
|
||||
import BadgeStandard from '../../components/BadgeStandard.astro'
|
||||
import Button from '../../components/Button.astro'
|
||||
import CommentSection from '../../components/CommentSection.astro'
|
||||
import CommentSummary from '../../components/CommentSummary.astro'
|
||||
import DropdownButton from '../../components/DropdownButton.astro'
|
||||
@@ -411,6 +410,8 @@ const serviceVisibilityInfo = getServiceVisibilityInfo(service.serviceVisibility
|
||||
const activeAlertOrWarningEvents = service.events.filter(
|
||||
(event) => getEventTypeInfo(event.type).showBanner && (event.endedAt === null || event.endedAt >= now)
|
||||
)
|
||||
const activeEventToShow =
|
||||
activeAlertOrWarningEvents.find((event) => event.type === EventType.ALERT) ?? activeAlertOrWarningEvents[0]
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
@@ -512,27 +513,23 @@ const activeAlertOrWarningEvents = service.events.filter(
|
||||
]}
|
||||
>
|
||||
{
|
||||
activeAlertOrWarningEvents.length > 0 && (
|
||||
!!activeEventToShow && (
|
||||
<a
|
||||
href="#events"
|
||||
class={cn(
|
||||
'mb-4 block rounded-md px-3 py-2 text-sm font-medium',
|
||||
activeAlertOrWarningEvents.some((e) => e.type === EventType.ALERT)
|
||||
? 'bg-red-900/50 text-red-300 hover:bg-red-800/60'
|
||||
: 'bg-yellow-900/50 text-yellow-300 hover:bg-yellow-800/60'
|
||||
'group mb-4 block rounded-md px-3 py-2 text-sm transition-colors duration-200',
|
||||
activeEventToShow.type === EventType.ALERT
|
||||
? 'bg-red-900/50 text-red-300 hover:bg-red-800/60 focus-visible:bg-red-800/60'
|
||||
: 'bg-yellow-900/50 text-yellow-300 hover:bg-yellow-800/60 focus-visible:bg-yellow-800/60'
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
name={
|
||||
activeAlertOrWarningEvents.some((e) => e.type === EventType.ALERT)
|
||||
? 'ri:alert-fill'
|
||||
: 'ri:alarm-warning-fill'
|
||||
}
|
||||
name={activeEventToShow.type === EventType.ALERT ? 'ri:alert-fill' : 'ri:alarm-warning-fill'}
|
||||
class="me-1.5 inline-block size-4 align-[-0.15em]"
|
||||
/>
|
||||
{activeAlertOrWarningEvents.some((e) => e.type === EventType.ALERT)
|
||||
? 'There is an active alert for this service. Click to see details.'
|
||||
: 'There is an active warning for this service. Click to see details.'}
|
||||
<span class="font-bold">{activeEventToShow.title}</span> — {activeEventToShow.content}
|
||||
{activeAlertOrWarningEvents.length >= 2 && <>+{activeAlertOrWarningEvents.length - 1} more events.</>}
|
||||
<span class="underline">Go to events</span>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
@@ -1081,105 +1078,119 @@ const activeAlertOrWarningEvents = service.events.filter(
|
||||
</h2>
|
||||
{
|
||||
service.verificationStatus === 'VERIFICATION_SUCCESS' || service.verificationStatus === 'APPROVED' ? (
|
||||
service.tosReview ? (
|
||||
<>
|
||||
<Schema
|
||||
item={
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Review',
|
||||
itemReviewed: { '@id': itemReviewedId },
|
||||
reviewAspect: 'Terms of Service',
|
||||
name: 'Terms of Service Review',
|
||||
reviewBody: service.tosReview.summary,
|
||||
datePublished: service.tosReviewAt?.toISOString(),
|
||||
author: KYCNOTME_SCHEMA_MINI,
|
||||
positiveNotes: service.tosReview.highlights
|
||||
.filter((h) => h.rating === 'positive')
|
||||
.map(
|
||||
(h, i) =>
|
||||
({
|
||||
'@type': 'ListItem',
|
||||
position: i + 1,
|
||||
name: h.title,
|
||||
description: h.content,
|
||||
}) satisfies ListItem
|
||||
),
|
||||
negativeNotes: service.tosReview.highlights
|
||||
.filter((h) => h.rating === 'negative')
|
||||
.map(
|
||||
(h, i) =>
|
||||
({
|
||||
'@type': 'ListItem',
|
||||
position: i + 1,
|
||||
name: h.title,
|
||||
description: h.content,
|
||||
}) satisfies ListItem
|
||||
),
|
||||
} satisfies WithContext<Review>
|
||||
}
|
||||
/>
|
||||
service.tosReviewAt !== null ? (
|
||||
service.tosReview ? (
|
||||
<>
|
||||
<Schema
|
||||
item={
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Review',
|
||||
itemReviewed: { '@id': itemReviewedId },
|
||||
reviewAspect: 'Terms of Service',
|
||||
name: 'Terms of Service Review',
|
||||
reviewBody: service.tosReview.summary,
|
||||
datePublished: service.tosReviewAt.toISOString(),
|
||||
author: KYCNOTME_SCHEMA_MINI,
|
||||
positiveNotes: service.tosReview.highlights
|
||||
.filter((h) => h.rating === 'positive')
|
||||
.map(
|
||||
(h, i) =>
|
||||
({
|
||||
'@type': 'ListItem',
|
||||
position: i + 1,
|
||||
name: h.title,
|
||||
description: h.content,
|
||||
}) satisfies ListItem
|
||||
),
|
||||
negativeNotes: service.tosReview.highlights
|
||||
.filter((h) => h.rating === 'negative')
|
||||
.map(
|
||||
(h, i) =>
|
||||
({
|
||||
'@type': 'ListItem',
|
||||
position: i + 1,
|
||||
name: h.title,
|
||||
description: h.content,
|
||||
}) satisfies ListItem
|
||||
),
|
||||
} satisfies WithContext<Review>
|
||||
}
|
||||
/>
|
||||
|
||||
<div class="flex flex-col sm:flex-row sm:gap-6">
|
||||
<div class="prose prose-invert prose-sm bg-night-800 max-w-none flex-1 rounded-lg px-4 py-2 text-pretty sm:px-6 sm:py-4">
|
||||
{service.tosReview.summary ? (
|
||||
<Markdown content={service.tosReview.summary} />
|
||||
) : (
|
||||
<p>No summary available</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 grid gap-4 sm:grid-cols-2 sm:gap-6">
|
||||
{sortBy(
|
||||
service.tosReview.highlights.map((highlight) => ({
|
||||
...highlight,
|
||||
ratingInfo: getTosHighlightRatingInfo(highlight.rating),
|
||||
})),
|
||||
'ratingInfo.order'
|
||||
).map((highlight) => (
|
||||
<div
|
||||
class={cn(
|
||||
'border-l-2 border-l-transparent pl-2 text-pretty',
|
||||
highlight.ratingInfo.classNames.borderColor
|
||||
)}
|
||||
>
|
||||
<h3 class="font-title font-bold">
|
||||
<Icon
|
||||
name={highlight.ratingInfo.icon}
|
||||
class={cn(
|
||||
highlight.ratingInfo.classNames.icon,
|
||||
'me-0.5 inline-block size-4 align-[-0.1em]'
|
||||
)}
|
||||
/>
|
||||
<span>{highlight.title}</span>
|
||||
</h3>
|
||||
{!!highlight.content && (
|
||||
<div class="prose prose-invert prose-sm">
|
||||
<Markdown content={highlight.content} />
|
||||
</div>
|
||||
<div class="flex flex-col sm:flex-row sm:gap-6">
|
||||
<div class="prose prose-invert prose-sm bg-night-800 max-w-none flex-1 rounded-lg px-4 py-2 text-pretty sm:px-6 sm:py-4">
|
||||
{service.tosReview.summary ? (
|
||||
<Markdown content={service.tosReview.summary} />
|
||||
) : (
|
||||
<p>No summary available</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-day-400 mt-4 text-center text-xs">
|
||||
{!!service.tosReviewAt && (
|
||||
<>
|
||||
Reviewed <TimeFormatted date={service.tosReviewAt} hourPrecision />
|
||||
</>
|
||||
<div class="mt-6 grid gap-4 sm:grid-cols-2 sm:gap-6">
|
||||
{sortBy(
|
||||
service.tosReview.highlights.map((highlight) => ({
|
||||
...highlight,
|
||||
ratingInfo: getTosHighlightRatingInfo(highlight.rating),
|
||||
})),
|
||||
'ratingInfo.order'
|
||||
).map((highlight) => (
|
||||
<div
|
||||
class={cn(
|
||||
'border-l-2 border-l-transparent pl-2 text-pretty',
|
||||
highlight.ratingInfo.classNames.borderColor
|
||||
)}
|
||||
>
|
||||
<h3 class="font-title font-bold">
|
||||
<Icon
|
||||
name={highlight.ratingInfo.icon}
|
||||
class={cn(
|
||||
highlight.ratingInfo.classNames.icon,
|
||||
'me-0.5 inline-block size-4 align-[-0.1em]'
|
||||
)}
|
||||
/>
|
||||
<span>{highlight.title}</span>
|
||||
</h3>
|
||||
{!!highlight.content && (
|
||||
<div class="prose prose-invert prose-sm">
|
||||
<Markdown content={highlight.content} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p class="text-day-400 mt-4 text-center text-xs">
|
||||
Reviewed <TimeFormatted date={service.tosReviewAt} hourPrecision />
|
||||
{service.tosUrls.length > 0 && 'from'}
|
||||
{service.tosUrls.map((url) => (
|
||||
<a href={url} class="cursor-pointer hover:underline">
|
||||
{urlDomain(url)}
|
||||
</a>
|
||||
))}
|
||||
</p>
|
||||
<p class="text-day-600 hover:text-day-400 mb-2 text-center text-xs duration-400">
|
||||
ToS reviews are AI-generated and should be used as a reference only.
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<div class="text-day-400 my-12 text-center">
|
||||
<p>Can't analyze ToS with AI</p>
|
||||
<p class="text-day-500 mt-1 text-sm text-balance">
|
||||
Maybe due to captchas, client side rendering, DDoS protections, or non-text format.
|
||||
</p>
|
||||
{service.tosUrls.length > 0 && (
|
||||
<p class="mt-2 text-xs">
|
||||
{service.tosUrls.map((url) => (
|
||||
<a href={url} class="hover:underline">
|
||||
{urlDomain(url)}
|
||||
</a>
|
||||
))}
|
||||
</p>
|
||||
)}
|
||||
{!!service.tosReviewAt && !!service.tosUrls.length && 'from'}
|
||||
{service.tosUrls.map((url) => (
|
||||
<a href={url} class="cursor-pointer hover:underline">
|
||||
{urlDomain(url)}
|
||||
</a>
|
||||
))}
|
||||
</p>
|
||||
<p class="text-day-600 hover:text-day-400 mb-2 text-center text-xs duration-400">
|
||||
ToS reviews are AI-generated and should be used as a reference only.
|
||||
</p>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div class="text-day-400 my-12 text-center">
|
||||
<p>Not reviewed yet</p>
|
||||
@@ -1332,77 +1343,6 @@ const activeAlertOrWarningEvents = service.events.filter(
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
service.verificationSteps.length > 0 && (
|
||||
<div class="border-night-500 mt-2 border-t pt-3">
|
||||
<h3 class="font-title mb-2 flex items-center gap-1 text-sm font-semibold">
|
||||
<Icon name="ri:checkbox-multiple-line" class="size-4" />
|
||||
Verification Steps
|
||||
</h3>
|
||||
<ul class="grid gap-2 text-xs">
|
||||
{service.verificationSteps.map((step) => {
|
||||
const statusInfo = getVerificationStepStatusInfo(step.status)
|
||||
return (
|
||||
<li>
|
||||
<details class="group/step">
|
||||
<summary class="border-night-600 hover:bg-night-700/50 relative flex cursor-pointer items-center gap-2 rounded p-1.5 select-none">
|
||||
<div
|
||||
class={cn('h-1.5 w-1.5 rounded-full', {
|
||||
'bg-gray-400 shadow-[0_0_3px_1px_rgba(156,163,175,0.7)]':
|
||||
step.status === VerificationStepStatus.PENDING,
|
||||
'bg-blue-400 shadow-[0_0_4px_1px_rgba(96,165,250,0.8)]':
|
||||
step.status === VerificationStepStatus.IN_PROGRESS,
|
||||
'bg-green-400 shadow-[0_0_4px_1px_rgba(34,197,94,0.8)]':
|
||||
step.status === VerificationStepStatus.PASSED,
|
||||
'bg-red-400 shadow-[0_0_4px_1px_rgba(248,113,113,0.8)]':
|
||||
step.status === VerificationStepStatus.FAILED,
|
||||
})}
|
||||
/>
|
||||
<div class="flex flex-1 items-center">
|
||||
<strong class="mr-1 font-medium text-white">{step.title}</strong>
|
||||
<span class="text-day-400 mr-1">•</span>
|
||||
<span
|
||||
class={cn('mr-2 font-bold', {
|
||||
'text-gray-400': step.status === VerificationStepStatus.PENDING,
|
||||
'text-blue-400': step.status === VerificationStepStatus.IN_PROGRESS,
|
||||
'text-green-400': step.status === VerificationStepStatus.PASSED,
|
||||
'text-red-400': step.status === VerificationStepStatus.FAILED,
|
||||
})}
|
||||
>
|
||||
{statusInfo.text}
|
||||
</span>
|
||||
<Icon
|
||||
name="ri:arrow-down-s-line"
|
||||
class="text-day-400 size-3.5 transition-transform group-open/step:rotate-180"
|
||||
/>
|
||||
</div>
|
||||
</summary>
|
||||
<div class="border-night-600 mt-1 ml-3.5 border-l pb-1 pl-3">
|
||||
<p class="text-day-400 text-2xs mb-1 font-medium">
|
||||
Added
|
||||
<TimeFormatted date={step.createdAt} hourPrecision />
|
||||
</p>
|
||||
{step.description && (
|
||||
<div class="text-day-300 my-1 text-xs">{step.description}</div>
|
||||
)}
|
||||
{step.evidenceMd && (
|
||||
<div class="bg-night-900 mt-2 rounded p-2">
|
||||
<div class="text-day-400 text-2xs mb-1 font-medium">Evidence:</div>
|
||||
<div class="prose prose-invert prose-xs prose-img:max-w-full text-[11px] leading-relaxed">
|
||||
<Markdown content={step.evidenceMd} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</details>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
@@ -1456,89 +1396,64 @@ const activeAlertOrWarningEvents = service.events.filter(
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
service.verificationProofMd && (
|
||||
<Button
|
||||
as="label"
|
||||
for="show-verification-proof"
|
||||
icon="ri:file-text-line"
|
||||
label="Verification audit"
|
||||
color="white"
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
service.verificationProofMd && (
|
||||
<>
|
||||
<input type="checkbox" id="show-verification-proof" class="peer sr-only" />
|
||||
<div class="bg-night-800 mt-4 hidden rounded-md p-4 peer-checked:block" id="verification-proof">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="font-title text-lg font-semibold">Verification proof</h3>
|
||||
<Button as="label" for="show-verification-proof" label="Close" icon="ri:close-line" size="sm" />
|
||||
</div>
|
||||
<div class="prose prose-invert prose-sm mt-4 max-w-none">
|
||||
<Markdown content={service.verificationProofMd} />
|
||||
</div>
|
||||
{service.verificationSteps.length > 0 && (
|
||||
<div class="mt-6">
|
||||
<h4 class="font-title text-md mb-3 font-semibold">Verification Steps</h4>
|
||||
<ul class="-mb-4">
|
||||
{service.verificationSteps.map((step) => {
|
||||
const statusInfo = getVerificationStepStatusInfo(step.status)
|
||||
|
||||
return (
|
||||
<li class="flex pb-4">
|
||||
<div class="border-night-500 bg-night-700/50 flex-grow rounded-md border p-2.5 shadow-sm">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<BadgeSmall
|
||||
text={statusInfo.text}
|
||||
icon={statusInfo.icon}
|
||||
color={statusInfo.color}
|
||||
inlineIcon={statusInfo.icon === 'ri:loader-4-line'}
|
||||
class={cn(
|
||||
{ 'animate-spin': statusInfo.icon === 'ri:loader-4-line' },
|
||||
'shrink-0'
|
||||
)}
|
||||
/>
|
||||
<strong class="text-sm leading-tight font-semibold text-white">
|
||||
{step.title}
|
||||
</strong>
|
||||
</div>
|
||||
<p class="text-day-300 mt-1.5 text-xs">{step.description}</p>
|
||||
</div>
|
||||
<p class="text-2xs text-day-400 shrink-0 pt-0.5 whitespace-nowrap">
|
||||
<TimeFormatted date={step.updatedAt} prefix={false} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{step.evidenceMd && (
|
||||
<details class="group/evidence mt-2 text-xs">
|
||||
<summary class="text-day-400 hover:text-day-200 cursor-pointer py-0.5 text-[11px] font-medium">
|
||||
Show Evidence
|
||||
</summary>
|
||||
<div class="prose prose-invert prose-xs bg-night-900 mt-1.5 rounded p-2 text-[11px] leading-relaxed group-open/evidence:block">
|
||||
<Markdown content={step.evidenceMd} />
|
||||
</div>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
service.verificationProofMd && (
|
||||
<>
|
||||
<h3 class="font-title mt-4 text-lg font-semibold">Verification proof</h3>
|
||||
<div class="prose prose-invert prose-sm max-w-none">
|
||||
<Markdown content={service.verificationProofMd} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
service.verificationSteps.length > 0 && (
|
||||
<>
|
||||
<h3 class="font-title text-md mt-6 mb-2 font-semibold">Verification Steps</h3>
|
||||
<ul class="mb-8 space-y-2">
|
||||
{service.verificationSteps.map((step) => {
|
||||
const statusInfo = getVerificationStepStatusInfo(step.status)
|
||||
|
||||
return (
|
||||
<li class="border-night-500 bg-night-700/50 flex-grow rounded-md border p-2.5 shadow-sm">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="flex flex-col">
|
||||
<div class="text-sm leading-tight font-semibold text-white">
|
||||
<BadgeSmall
|
||||
text={statusInfo.text}
|
||||
icon={statusInfo.icon}
|
||||
color={statusInfo.color}
|
||||
inlineIcon={statusInfo.icon === 'ri:loader-4-line'}
|
||||
classNames={{
|
||||
icon: cn(statusInfo.icon === 'ri:loader-4-line' && 'animate-spin'),
|
||||
}}
|
||||
class="me-1"
|
||||
/>
|
||||
|
||||
{step.title}
|
||||
</div>
|
||||
<p class="text-day-300 mt-1.5 text-xs">{step.description}</p>
|
||||
</div>
|
||||
<p class="text-2xs text-day-400 shrink-0 pt-0.5 whitespace-nowrap">
|
||||
<TimeFormatted date={step.updatedAt} prefix={false} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{step.evidenceMd && (
|
||||
<div class="prose prose-invert prose-xs mt-2 block max-w-none text-xs leading-relaxed">
|
||||
<Markdown content={step.evidenceMd} />
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
<h2 class="font-title border-day-500 text-day-200 mt-2 mb-3 border-b text-lg font-bold" id="comments">
|
||||
Comments
|
||||
|
||||
Reference in New Issue
Block a user