Merge pull request #235 from amal66/olp-pr/lint-burndown

[Lint 04] fix: burn down frontend eslint errors; make CI lint blocking
This commit is contained in:
Will Chen 2026-07-22 16:33:52 +08:00 committed by GitHub
commit 7d2ba4b87f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 27 additions and 12 deletions

View file

@ -7,7 +7,7 @@
# on a tree without those pieces the test steps no-op and the build still # on a tree without those pieces the test steps no-op and the build still
# gates the merge. Beyond the fork version this also builds the frontend # gates the merge. Beyond the fork version this also builds the frontend
# (placeholder NEXT_PUBLIC_* env — verified sufficient for `next build`) and # (placeholder NEXT_PUBLIC_* env — verified sufficient for `next build`) and
# runs eslint as an advisory step (see the comment on that step). # runs eslint as a blocking gate (the error backlog is at zero).
name: CI name: CI
on: on:
@ -61,11 +61,9 @@ jobs:
# harness PR merges); runs the suite once it exists. # harness PR merges); runs the suite once it exists.
- run: npm test --if-present - run: npm test --if-present
# Advisory only for now: main currently carries 23 eslint errors, so a # Blocking gate: the eslint error backlog was burned down in this PR
# blocking gate would turn every PR red on inherited debt. Flip this to # (0 errors; warnings do not fail the step), so any new error fails CI.
# blocking (remove continue-on-error) once the backlog is burned down.
- run: npm run lint - run: npm run lint
continue-on-error: true
# Production build. NEXT_PUBLIC_* values are inlined at build time and # Production build. NEXT_PUBLIC_* values are inlined at build time and
# only need to be well-formed here — nothing is contacted during build. # only need to be well-formed here — nothing is contacted during build.

View file

@ -242,6 +242,7 @@ export function AskInputPopup({
}; };
useEffect(() => { useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- auto-submit when every question is answered; submit() sets state as part of the side effect
if (canSubmit) submit(); if (canSubmit) submit();
}); });

View file

@ -216,6 +216,7 @@ export function CaseLawPanel({
useEffect(() => { useEffect(() => {
if (tab.opinions?.length) { if (tab.opinions?.length) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- sync path of an async fetch effect: serve prop/cache data without a loading flash
setOpinions(tab.opinions); setOpinions(tab.opinions);
setLoading(false); setLoading(false);
setError(null); setError(null);
@ -269,10 +270,12 @@ export function CaseLawPanel({
orderOpinions(opinions).find( orderOpinions(opinions).find(
({ opinion }) => typeof opinion.opinionId === "number", ({ opinion }) => typeof opinion.opinionId === "number",
)?.opinion.opinionId ?? null; )?.opinion.opinionId ?? null;
// eslint-disable-next-line react-hooks/set-state-in-effect -- reset active opinion after opinions load
setActiveOpinionId(firstOpinionId); setActiveOpinionId(firstOpinionId);
}, [opinions]); }, [opinions]);
useEffect(() => { useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- sync quote list when the tab prop changes
setRelevantQuotes(tab.quotes ?? []); setRelevantQuotes(tab.quotes ?? []);
}, [tab.quotes]); }, [tab.quotes]);
@ -321,6 +324,7 @@ export function CaseLawPanel({
); );
useEffect(() => { useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- reset quote selection when the quote set changes
setQuoteIndexState({ cacheKey: quoteCacheKey, index: 0 }); setQuoteIndexState({ cacheKey: quoteCacheKey, index: 0 });
const firstQuote = relevantQuotes[0]; const firstQuote = relevantQuotes[0];
setActiveQuoteKey(firstQuote ? relevantQuoteKey(firstQuote, 0) : null); setActiveQuoteKey(firstQuote ? relevantQuoteKey(firstQuote, 0) : null);

View file

@ -83,6 +83,7 @@ export function ChatView({
const panelCloseTimerRef = useRef<number | null>(null); const panelCloseTimerRef = useRef<number | null>(null);
useEffect(() => { useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- reset per-chat UI state when switching chats
setHiddenAskInputKeys(new Set()); setHiddenAskInputKeys(new Set());
}, [chatId]); }, [chatId]);
@ -519,6 +520,7 @@ export function ChatView({
const c = messagesContainerRef.current; const c = messagesContainerRef.current;
if (!c) return; if (!c) return;
c.addEventListener("scroll", updateScrollButton); c.addEventListener("scroll", updateScrollButton);
// eslint-disable-next-line react-hooks/set-state-in-effect -- initial scroll-button state must be measured from the live DOM
updateScrollButton(); updateScrollButton();
return () => c.removeEventListener("scroll", updateScrollButton); return () => c.removeEventListener("scroll", updateScrollButton);
}, [messages, updateScrollButton]); }, [messages, updateScrollButton]);
@ -553,6 +555,7 @@ export function ChatView({
useEffect(() => { useEffect(() => {
if (messages.length === 0) { if (messages.length === 0) {
hasScrolledRef.current = false; hasScrolledRef.current = false;
// eslint-disable-next-line react-hooks/set-state-in-effect -- hide messages until scroll position is restored to avoid a visible jump
setMessagesVisible(false); setMessagesVisible(false);
} else if (!hasScrolledRef.current) { } else if (!hasScrolledRef.current) {
const userMsgCount = messages.filter( const userMsgCount = messages.filter(
@ -682,8 +685,8 @@ export function ChatView({
{msg.role === "user" ? ( {msg.role === "user" ? (
<UserMessage <UserMessage
content={msg.content ?? ""} content={msg.content ?? ""}
files={(msg as any).files} files={msg.files}
workflow={(msg as any).workflow} workflow={msg.workflow}
/> />
) : ( ) : (
<AssistantMessage <AssistantMessage
@ -692,11 +695,11 @@ export function ChatView({
i === messages.length - 1 && i === messages.length - 1 &&
isResponseLoading isResponseLoading
} }
isError={!!(msg as any).error} isError={!!msg.error}
errorMessage={ errorMessage={
typeof (msg as any) typeof msg.error ===
.error === "string" "string"
? (msg as any).error ? msg.error
: undefined : undefined
} }
citations={msg.citations} citations={msg.citations}

View file

@ -47,6 +47,7 @@ export function CitationQuotesHeader({
useEffect(() => { useEffect(() => {
if (!hasMultipleQuotes && viewMode === "list") { if (!hasMultipleQuotes && viewMode === "list") {
// eslint-disable-next-line react-hooks/set-state-in-effect -- collapse list view when quotes drop to a single item
setViewMode("single"); setViewMode("single");
} }
}, [hasMultipleQuotes, viewMode]); }, [hasMultipleQuotes, viewMode]);

View file

@ -29,6 +29,7 @@ export function PreResponseWrapper({
useEffect(() => { useEffect(() => {
if (forceOpen) { if (forceOpen) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- streaming open/minimize latch (see comment above)
setIsOpen(true); setIsOpen(true);
return; return;
} }

View file

@ -61,6 +61,7 @@ export function Modal({
// Portals can't render during SSR, so a keep-mounted modal only renders // Portals can't render during SSR, so a keep-mounted modal only renders
// (hidden) after the first client mount. // (hidden) after the first client mount.
const [hasMounted, setHasMounted] = useState(false); const [hasMounted, setHasMounted] = useState(false);
// eslint-disable-next-line react-hooks/set-state-in-effect -- SSR portal gate: must flip after first client mount
useEffect(() => setHasMounted(true), []); useEffect(() => setHasMounted(true), []);
const hasHeader = breadcrumbs?.length; const hasHeader = breadcrumbs?.length;
const hasFooter = const hasFooter =

View file

@ -21,6 +21,7 @@ export function MfaLoginGate({ children }: { children: ReactNode }) {
useEffect(() => { useEffect(() => {
if (!user) { if (!user) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- sync fast paths of the async MFA check effect
setGateState("idle"); setGateState("idle");
return; return;
} }
@ -64,6 +65,7 @@ export function MfaLoginGate({ children }: { children: ReactNode }) {
if (gateState === "required" && !isVerifyPage) { if (gateState === "required" && !isVerifyPage) {
if (hasRecentMfaVerification()) { if (hasRecentMfaVerification()) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- clear gate when a recent MFA verification exists instead of redirecting
setGateState("verified"); setGateState("verified");
return; return;
} }

View file

@ -164,6 +164,7 @@ function TRResponseStatus({ isActive }: { isActive: boolean }) {
useEffect(() => { useEffect(() => {
if (wasActiveRef.current && !isActive) { if (wasActiveRef.current && !isActive) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- timed 'Done' flash on the active->idle transition
setShowDone(true); setShowDone(true);
setDoneVisible(true); setDoneVisible(true);
const t = setTimeout(() => setDoneVisible(false), 1500); const t = setTimeout(() => setDoneVisible(false), 1500);

View file

@ -73,6 +73,7 @@ export function ChatHistoryProvider({ children }: { children: ReactNode }) {
useEffect(() => { useEffect(() => {
if (!user) { if (!user) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- clear chat state on logout inside the effect that loads chats
setChats([]); setChats([]);
setChatLimit(INITIAL_CHAT_LIMIT); setChatLimit(INITIAL_CHAT_LIMIT);
setHasMoreChats(false); setHasMoreChats(false);

View file

@ -49,6 +49,7 @@ export function useFetchDocxBytes(
useEffect(() => { useEffect(() => {
if (!documentId) { if (!documentId) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- clear stale bytes when documentId is removed, within the fetch effect
setBytes(null); setBytes(null);
setDownloadUrl(null); setDownloadUrl(null);
return; return;

View file

@ -16,6 +16,7 @@ export function useSelectedModel(): [string, (id: string) => void] {
const [model, setModelState] = useState<string>(DEFAULT_MODEL_ID); const [model, setModelState] = useState<string>(DEFAULT_MODEL_ID);
useEffect(() => { useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- hydration-safe localStorage read; SSR must render the default model
setModelState(readStored()); setModelState(readStored());
}, []); }, []);

View file

@ -228,7 +228,7 @@ export default function SupportPage() {
{/* Email Display (if logged in) */} {/* Email Display (if logged in) */}
{user?.email && ( {user?.email && (
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-500">
We'll respond to:{" "} We&apos;ll respond to:{" "}
<span className="font-medium"> <span className="font-medium">
{user.email} {user.email}
</span> </span>