Repository navigation
fix(ci): restore the post-Wave-9 frontend verification matrix #562
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d386c28
a266dd4
68c6240
4609289
4c14077
7bb8c58
95f8ec9
b390a37
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,9 +3,6 @@ | |
|
|
||
| name: CI | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
|
|
||
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,78 +1,54 @@ | ||
|
|
||
| import { Evidence } from "@/app/types/dispute"; | ||
| import { ExternalLink, FileText, LinkIcon, ShieldAlert } from "lucide-react"; | ||
| import { sanitizeText, safeUrl } from "@/lib/security/evidence-sanitizer"; | ||
| import { SafeExternalLink } from "@/components/security/SafeExternalLink"; | ||
| import { Evidence } from '@/app/types/dispute'; | ||
| import { FileText, LinkIcon } from 'lucide-react'; | ||
| import { sanitizeText } from '@/lib/security/evidence-sanitizer'; | ||
| import { PrivateEvidenceLink } from '@/components/security/PrivateEvidenceLink'; | ||
|
|
||
| /** | ||
| * V2-FE-075 — Evidence links are untrusted API content. Titles, descriptions | ||
| * and URLs are sanitized; unsafe URLs fail closed to an accessible blocked | ||
| * placeholder instead of an anchor. | ||
| * | ||
| * V2-FE Evidence Privacy Protection — URLs displayed with truncation for | ||
| * privacy, full URLs available via clipboard. | ||
| * Evidence links are untrusted API content. Text is sanitized and URLs are | ||
| * rendered through the privacy-preserving, fail-closed link boundary. | ||
| */ | ||
| export const EvidenceLinks = ({ evidences }: { evidences: Evidence[] }) => { | ||
| return ( | ||
| <div className="bg-[#13141b] border border-gray-800 rounded-xl p-6 mb-6"> | ||
| <div className="flex items-center space-x-2 text-white font-medium mb-4"> | ||
| <LinkIcon size={18} /> | ||
| <LinkIcon size={18} aria-hidden="true" /> | ||
| <h2>Evidence Links</h2> | ||
| </div> | ||
| <div className="space-y-3"> | ||
| {evidences.map((evidence) => { | ||
| const title = sanitizeText(evidence.title, 300); | ||
| const description = sanitizeText(evidence.description, 600); | ||
| const urlCheck = safeUrl(evidence.url); | ||
|
|
||
| return ( | ||
| <div | ||
| key={evidence.id} | ||
| className="flex items-center justify-between gap-3 p-4 rounded-lg border border-gray-800 bg-[#0a0a0f] hover:border-gray-700 transition-colors" | ||
| > | ||
| <div className="flex min-w-0 items-center space-x-4"> | ||
| <FileText className="shrink-0 text-gray-500" size={20} aria-hidden="true" /> | ||
| <FileText | ||
| className="shrink-0 text-gray-500" | ||
| size={20} | ||
| aria-hidden="true" | ||
| /> | ||
| <div className="min-w-0"> | ||
| <p className="truncate text-sm font-medium text-gray-200">{title}</p> | ||
| {description && ( | ||
| <p className="truncate text-sm font-medium text-gray-200"> | ||
| {title || 'Evidence'} | ||
| </p> | ||
| {description ? ( | ||
| <p className="truncate text-xs text-gray-500">{description}</p> | ||
| )} | ||
| ) : null} | ||
| </div> | ||
| </div> | ||
| <PrivateEvidenceLink | ||
| href={evidence.url} | ||
| className="shrink-0 text-sm text-gray-400 hover:text-white flex items-center transition-colors" | ||
| ariaLabel={`View evidence: ${title} (truncated for privacy, click to copy)`} | ||
| showIcon={true} | ||
| className="shrink-0 text-sm" | ||
| ariaLabel={`View evidence: ${title || 'link'} (URL truncated for privacy)`} | ||
| showIcon | ||
| /> | ||
| <p className="truncate text-sm font-medium text-gray-200">{title || evidence.url}</p> | ||
| <p className="truncate text-xs text-gray-500">{description}</p> | ||
| </div> | ||
| </div> | ||
| {validation.isValid && safeHref ? ( | ||
| <a | ||
| href={safeHref} | ||
| target="_blank" | ||
| rel="noopener noreferrer nofollow" | ||
| className="shrink-0 text-sm text-gray-400 hover:text-white flex items-center transition-colors" | ||
| aria-label={`View evidence: ${title || description || "link"} (opens in new tab)`} | ||
| > | ||
| View <ExternalLink size={14} className="ml-1" aria-hidden="true" /> | ||
| </SafeExternalLink> | ||
| ) : ( | ||
| <span | ||
| className="shrink-0 text-sm text-gray-600 flex items-center" | ||
| role="img" | ||
| aria-label="Evidence link blocked for security reasons" | ||
| > | ||
| <ShieldAlert size={14} className="mr-1" aria-hidden="true" /> | ||
| Blocked link | ||
| </span> | ||
| )} | ||
| </div> | ||
| ); | ||
| })} | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,131 +1,145 @@ | ||
| import React from 'react'; | ||
|
|
||
| export type TransactionStatusValue = | ||
| | 'idle' | ||
| | 'pending' | ||
| | 'preparing' | ||
| | 'signature-requested' | ||
| | 'submitted' | ||
| | 'confirming' | ||
| | 'safe' | ||
| | 'indexing' | ||
| | 'stale' | ||
| | 'success' | ||
| | 'finalized' | ||
| | 'error' | ||
| | 'dropped' | ||
| | 'replaced' | ||
| | 'reverted' | ||
| | 'rejected' | ||
| | 'reorged' | ||
| | 'failed'; | ||
|
|
||
| export type TransactionStatusMessages = Partial< | ||
| Record<TransactionStatusValue, string> | ||
| >; | ||
|
|
||
| interface TransactionStatusProps { | ||
| status: TransactionStatusValue; | ||
| messages?: TransactionStatusMessages; | ||
| errorMessage?: string; | ||
| onRetry?: () => void; | ||
| } | ||
|
|
||
| const errorStatuses = new Set<TransactionStatusValue>([ | ||
| "error", | ||
| "dropped", | ||
| "replaced", | ||
| "reverted", | ||
| "rejected", | ||
| "reorged", | ||
| "failed", | ||
| 'error', | ||
| 'dropped', | ||
| 'replaced', | ||
| 'reverted', | ||
| 'rejected', | ||
| 'reorged', | ||
| 'failed', | ||
| ]); | ||
|
|
||
| const busyStatuses = new Set<TransactionStatusValue>([ | ||
| "pending", | ||
| "preparing", | ||
| "signature-requested", | ||
| "submitted", | ||
| "confirming", | ||
| "safe", | ||
| "indexing", | ||
| "stale", | ||
| 'pending', | ||
| 'preparing', | ||
| 'signature-requested', | ||
| 'submitted', | ||
| 'confirming', | ||
| 'safe', | ||
| 'indexing', | ||
| 'stale', | ||
| ]); | ||
|
|
||
| const defaultMessages: Record<TransactionStatusValue, string> = { | ||
| idle: '', | ||
| pending: 'Transaction pending...', | ||
| preparing: 'Preparing transaction...', | ||
| 'signature-requested': 'Confirm the transaction in your wallet.', | ||
| submitted: 'Transaction submitted.', | ||
| confirming: 'Transaction confirming...', | ||
| safe: 'Transaction is safe.', | ||
| indexing: 'Transaction confirmed; waiting for indexing.', | ||
| stale: 'Transaction status may be stale.', | ||
| success: 'Verification submitted.', | ||
| finalized: 'Transaction finalized.', | ||
| error: 'Transaction failed.', | ||
| dropped: 'Transaction was dropped.', | ||
| replaced: 'Transaction was replaced.', | ||
| reverted: 'Transaction reverted.', | ||
| rejected: 'Transaction rejected.', | ||
| reorged: 'Transaction was affected by a chain reorganization.', | ||
| failed: 'Transaction failed.', | ||
| }; | ||
|
|
||
| export function TransactionStatus({ | ||
| status, | ||
| messages = {}, | ||
| }: { | ||
| status: TransactionStatusValue; | ||
| messages?: Partial<TransactionStatusMessages>; | ||
| }) { | ||
| if (status === "idle") return null; | ||
| errorMessage, | ||
| onRetry, | ||
| }: TransactionStatusProps) { | ||
| if (status === 'idle') return null; | ||
|
|
||
| if (status === 'pending') { | ||
| const message = messages[status] ?? defaultMessages[status]; | ||
|
|
||
| if (busyStatuses.has(status)) { | ||
| return ( | ||
| <div | ||
| role="status" | ||
| aria-live="polite" | ||
| className="flex items-center space-x-2 text-gray-600 dark:text-gray-300" | ||
| > | ||
| <svg | ||
| className="animate-spin h-4 w-4 text-blue-500" | ||
| className="motion-safe:animate-spin h-4 w-4 text-blue-500" | ||
| xmlns="http://www.w3.org/2000/svg" | ||
| fill="none" | ||
| viewBox="0 0 24 24" | ||
| aria-hidden="true" | ||
| > | ||
| <circle | ||
| className="opacity-25" | ||
| cx="12" | ||
| cy="12" | ||
| r="10" | ||
| stroke="currentColor" | ||
| strokeWidth="4" | ||
| /> | ||
| <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" /> | ||
| <path | ||
| className="opacity-75" | ||
| fill="currentColor" | ||
| d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" | ||
| /> | ||
| </svg> | ||
| <p className="text-sm font-medium">Transaction pending...</p> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| if (status === 'success') { | ||
| return ( | ||
| <div | ||
| role="alert" | ||
| aria-live="assertive" | ||
| className="flex items-center space-x-2 text-green-600 dark:text-green-400" | ||
| > | ||
| <svg | ||
| className="h-4 w-4" | ||
| xmlns="http://www.w3.org/2000/svg" | ||
| viewBox="0 0 20 20" | ||
| fill="currentColor" | ||
| > | ||
| <path | ||
| fillRule="evenodd" | ||
| d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" | ||
| clipRule="evenodd" | ||
| /> | ||
| </svg> | ||
| <p className="text-sm font-medium">Verification submitted</p> | ||
| <p className="text-sm font-medium">{message}</p> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| if (status === 'error') { | ||
| if (errorStatuses.has(status)) { | ||
| return ( | ||
| <div | ||
| role="alert" | ||
| aria-live="assertive" | ||
| className="flex flex-col space-y-2 text-red-600 dark:text-red-400" | ||
| > | ||
| <div className="flex items-center space-x-2"> | ||
| <svg | ||
| className="h-4 w-4" | ||
| xmlns="http://www.w3.org/2000/svg" | ||
| viewBox="0 0 20 20" | ||
| fill="currentColor" | ||
| > | ||
| <path | ||
| fillRule="evenodd" | ||
| d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" | ||
| clipRule="evenodd" | ||
| /> | ||
| </svg> | ||
| <p className="text-sm font-medium">Transaction failed</p> | ||
| </div> | ||
| {errorMessage && ( | ||
| <p className="text-xs text-red-500 dark:text-red-300"> | ||
| {errorMessage} | ||
| </p> | ||
| )} | ||
| {onRetry && ( | ||
| <p className="text-sm font-medium">{message}</p> | ||
| {errorMessage ? ( | ||
| <p className="text-xs text-red-500 dark:text-red-300">{errorMessage}</p> | ||
| ) : null} | ||
| {onRetry ? ( | ||
| <button | ||
| type="button" | ||
| onClick={onRetry} | ||
| className="self-start text-xs underline hover:text-red-700 dark:hover:text-red-200 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 rounded" | ||
| aria-label="Retry transaction" | ||
| className="self-start rounded text-xs underline focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2" | ||
| > | ||
| Retry | ||
| Retry transaction | ||
| </button> | ||
| )} | ||
| ) : null} | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| return null; | ||
| return ( | ||
| <div | ||
| role="status" | ||
| aria-live="polite" | ||
| className="flex items-center space-x-2 text-green-600 dark:text-green-400" | ||
| > | ||
| <p className="text-sm font-medium">{message}</p> | ||
| </div> | ||
| ); | ||
|
Comment on lines
+136
to
+144
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '1,110p' src/components/features/claim-verification/VerificationActions.tsx
sed -n '20,42p;115,135p' docs/UI_STATE_MODEL.md
sed -n '55,85p;130,148p' src/components/features/claim-verification/TransactionStatus.tsxRepository: DigiNodes/truthbounty-frontend Length of output: 6750 Render only finalized transactions with success styling.
Suggested fix+const successStatuses = new Set<TransactionStatusValue>(['finalized']);
+
...
+ if (status === 'success') {
+ return (
+ <div
+ role="status"
+ aria-live="polite"
+ className="flex items-center space-x-2 text-gray-600 dark:text-gray-300"
+ >
+ <p className="text-sm font-medium">{message}</p>
+ </div>
+ );
+ }
+
+ if (!successStatuses.has(status)) return null;
+
return (🤖 Prompt for AI Agents |
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
git diff 5762d37048601ee8084ea111c00fafece181910e b390a37a5179bf27e01ae2efa16abc08535882e6 -- src/components/features/claim-details/EvidenceLinks.tsx sed -n '45,140p' src/components/security/PrivateEvidenceLink.tsxRepository: DigiNodes/truthbounty-frontend
Length of output: 7709
🏁 Script executed:
Repository: DigiNodes/truthbounty-frontend
Length of output: 22063
🏁 Script executed:
Repository: DigiNodes/truthbounty-frontend
Length of output: 4444
🏁 Script executed:
Repository: DigiNodes/truthbounty-frontend
Length of output: 3863
Keep the evidence link’s accessible name aligned with its action.
PrivateEvidenceLinkalready prevents navigation and copies the URL when its anchor is clicked. This PR removes the separate external anchor from this list and labels the remaining copy-only link “View evidence.” Screen-reader users are told to expect navigation but activate a copy action. Add an opt-in navigation mode for this use and useshowCopyButtonfor copying. Keep copy-on-click as the component default for its other callers.Keep navigation and copying as separate actions
🤖 Prompt for AI Agents