/ frontend / src / components / emergency / EmergencyPanel.tsx
EmergencyPanel.tsx
  1  import { useState } from 'react'
  2  import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
  3  import { useAuthStore } from '../../store/auth'
  4  import type { Chain } from '../../types/vote'
  5  import type { EmergencyAction, EmergencyActionType } from '../../types/emergency'
  6  import * as emergencyService from '../../services/emergency'
  7  
  8  interface EmergencyPanelProps {
  9    chain: Chain
 10  }
 11  
 12  const ACTION_TYPES: { type: EmergencyActionType; label: string; description: string }[] = [
 13    {
 14      type: 'pause_minting',
 15      label: 'Pause Minting',
 16      description: 'Temporarily halt all token minting operations',
 17    },
 18    {
 19      type: 'resume_minting',
 20      label: 'Resume Minting',
 21      description: 'Restore normal minting operations',
 22    },
 23    {
 24      type: 'emergency_rollback',
 25      label: 'Emergency Rollback',
 26      description: 'Rollback to a previous checkpoint without governance vote',
 27    },
 28    {
 29      type: 'freeze_account',
 30      label: 'Freeze Account',
 31      description: 'Temporarily freeze a specific account',
 32    },
 33    {
 34      type: 'pause_governance',
 35      label: 'Pause Governance',
 36      description: 'Halt all governance voting temporarily',
 37    },
 38    {
 39      type: 'emergency_upgrade',
 40      label: 'Emergency Upgrade',
 41      description: 'Deploy critical security patch without standard timelock',
 42    },
 43  ]
 44  
 45  export default function EmergencyPanel({ chain }: EmergencyPanelProps) {
 46    const { address, isConnected } = useAuthStore()
 47    const queryClient = useQueryClient()
 48  
 49    const [selectedAction, setSelectedAction] = useState<EmergencyActionType | null>(null)
 50    const [reason, setReason] = useState('')
 51    const [params, setParams] = useState<Record<string, string>>({})
 52  
 53    // Check if user is DEQ member
 54    const { data: isDEQ = false } = useQuery({
 55      queryKey: ['isDEQMember', address, chain],
 56      queryFn: () => emergencyService.isDEQMember(address!, chain),
 57      enabled: !!address,
 58    })
 59  
 60    // Get pending actions
 61    const { data: pendingActions = [] } = useQuery({
 62      queryKey: ['pendingEmergencyActions', chain],
 63      queryFn: () => emergencyService.getPendingEmergencyActions(chain),
 64    })
 65  
 66    // Get emergency config
 67    const { data: config } = useQuery({
 68      queryKey: ['emergencyConfig', chain],
 69      queryFn: () => emergencyService.getEmergencyConfig(chain),
 70    })
 71  
 72    // Request action mutation
 73    const requestMutation = useMutation({
 74      mutationFn: () =>
 75        emergencyService.requestEmergencyAction(selectedAction!, chain, params, reason),
 76      onSuccess: () => {
 77        queryClient.invalidateQueries({ queryKey: ['pendingEmergencyActions', chain] })
 78        setSelectedAction(null)
 79        setReason('')
 80        setParams({})
 81      },
 82    })
 83  
 84    const actionInfo = selectedAction
 85      ? emergencyService.formatActionType(selectedAction)
 86      : null
 87  
 88    return (
 89      <div className="space-y-6">
 90        {/* Header */}
 91        <div className="bg-red-50 border border-red-200 rounded-lg p-4">
 92          <div className="flex items-center gap-2 text-red-800">
 93            <WarningIcon className="h-5 w-5" />
 94            <span className="font-semibold">Emergency Actions Panel</span>
 95          </div>
 96          <p className="text-sm text-red-700 mt-1">
 97            Emergency actions require {config?.requiredSignatures || 3} of{' '}
 98            {config?.totalDEQMembers || 5} DEQ member signatures.
 99          </p>
100        </div>
101  
102        {/* DEQ Status */}
103        {isConnected && (
104          <div
105            className={`rounded-lg p-4 ${
106              isDEQ
107                ? 'bg-green-50 border border-green-200'
108                : 'bg-gray-50 border border-gray-200'
109            }`}
110          >
111            <div className="flex items-center gap-2">
112              {isDEQ ? (
113                <>
114                  <CheckIcon className="h-5 w-5 text-green-600" />
115                  <span className="text-green-800 font-medium">
116                    You are a DEQ member
117                  </span>
118                </>
119              ) : (
120                <>
121                  <LockIcon className="h-5 w-5 text-gray-500" />
122                  <span className="text-gray-600">
123                    You are not a DEQ member. Contact governance to request access.
124                  </span>
125                </>
126              )}
127            </div>
128          </div>
129        )}
130  
131        {/* Pending Actions */}
132        {pendingActions.length > 0 && (
133          <div className="bg-white border rounded-lg">
134            <div className="p-4 border-b">
135              <h3 className="font-medium">Pending Actions ({pendingActions.length})</h3>
136            </div>
137            <div className="divide-y">
138              {pendingActions.map((action) => (
139                <PendingActionCard
140                  key={action.id}
141                  action={action}
142                  userAddress={address ?? undefined}
143                  isDEQ={isDEQ}
144                  chain={chain}
145                />
146              ))}
147            </div>
148          </div>
149        )}
150  
151        {/* Request New Action */}
152        {isDEQ && (
153          <div className="bg-white border rounded-lg">
154            <div className="p-4 border-b">
155              <h3 className="font-medium">Request Emergency Action</h3>
156            </div>
157  
158            <div className="p-4 space-y-4">
159              {/* Action Type Selection */}
160              <div>
161                <label className="block text-sm font-medium text-gray-700 mb-2">
162                  Action Type
163                </label>
164                <div className="grid grid-cols-2 gap-2">
165                  {ACTION_TYPES.map(({ type, label, description }) => {
166                    const info = emergencyService.formatActionType(type)
167                    return (
168                      <button
169                        key={type}
170                        onClick={() => setSelectedAction(type)}
171                        className={`p-3 rounded-lg border text-left transition-colors ${
172                          selectedAction === type
173                            ? 'border-red-500 bg-red-50'
174                            : 'border-gray-200 hover:border-gray-300'
175                        }`}
176                      >
177                        <div className="flex items-center gap-2">
178                          <span
179                            className={`text-xs px-2 py-0.5 rounded ${
180                              info.severity === 'critical'
181                                ? 'bg-red-100 text-red-700'
182                                : info.severity === 'high'
183                                ? 'bg-orange-100 text-orange-700'
184                                : 'bg-yellow-100 text-yellow-700'
185                            }`}
186                          >
187                            {info.severity}
188                          </span>
189                        </div>
190                        <div className="font-medium text-sm mt-1">{label}</div>
191                        <div className="text-xs text-gray-500 mt-1">{description}</div>
192                      </button>
193                    )
194                  })}
195                </div>
196              </div>
197  
198              {/* Action-specific params */}
199              {selectedAction === 'freeze_account' && (
200                <div>
201                  <label className="block text-sm font-medium text-gray-700 mb-1">
202                    Account Address
203                  </label>
204                  <input
205                    type="text"
206                    value={params.address || ''}
207                    onChange={(e) => setParams({ ...params, address: e.target.value })}
208                    placeholder="ax1... or dx1..."
209                    className="w-full px-3 py-2 border rounded-lg text-sm"
210                  />
211                </div>
212              )}
213  
214              {selectedAction === 'emergency_rollback' && (
215                <div>
216                  <label className="block text-sm font-medium text-gray-700 mb-1">
217                    Target Checkpoint Hash
218                  </label>
219                  <input
220                    type="text"
221                    value={params.checkpoint || ''}
222                    onChange={(e) => setParams({ ...params, checkpoint: e.target.value })}
223                    placeholder="abc123..."
224                    className="w-full px-3 py-2 border rounded-lg text-sm"
225                  />
226                </div>
227              )}
228  
229              {selectedAction === 'emergency_upgrade' && (
230                <div>
231                  <label className="block text-sm font-medium text-gray-700 mb-1">
232                    Program ID
233                  </label>
234                  <input
235                    type="text"
236                    value={params.programId || ''}
237                    onChange={(e) => setParams({ ...params, programId: e.target.value })}
238                    placeholder="program.adl"
239                    className="w-full px-3 py-2 border rounded-lg text-sm"
240                  />
241                </div>
242              )}
243  
244              {/* Reason */}
245              {selectedAction && (
246                <div>
247                  <label className="block text-sm font-medium text-gray-700 mb-1">
248                    Reason for Emergency Action
249                  </label>
250                  <textarea
251                    value={reason}
252                    onChange={(e) => setReason(e.target.value)}
253                    rows={3}
254                    placeholder="Describe the emergency situation and why this action is necessary..."
255                    className="w-full px-3 py-2 border rounded-lg text-sm"
256                  />
257                </div>
258              )}
259  
260              {/* Submit */}
261              {selectedAction && (
262                <div className="flex items-center gap-4">
263                  <button
264                    onClick={() => requestMutation.mutate()}
265                    disabled={!reason || requestMutation.isPending}
266                    className="px-4 py-2 bg-red-600 text-white rounded-lg text-sm font-medium disabled:opacity-50 hover:bg-red-700"
267                  >
268                    {requestMutation.isPending ? 'Submitting...' : 'Request Action'}
269                  </button>
270  
271                  {actionInfo && (
272                    <span className="text-sm text-gray-500">
273                      Severity: {actionInfo.severity}
274                    </span>
275                  )}
276                </div>
277              )}
278            </div>
279          </div>
280        )}
281      </div>
282    )
283  }
284  
285  function PendingActionCard({
286    action,
287    userAddress,
288    isDEQ,
289    chain,
290  }: {
291    action: EmergencyAction
292    userAddress: string | undefined
293    isDEQ: boolean
294    chain: Chain
295  }) {
296    const queryClient = useQueryClient()
297    const status = emergencyService.calculateActionStatus(action, userAddress, isDEQ)
298    const actionInfo = emergencyService.formatActionType(action.actionType)
299  
300    const signMutation = useMutation({
301      mutationFn: () => emergencyService.signEmergencyAction(action.id, chain),
302      onSuccess: () => {
303        queryClient.invalidateQueries({ queryKey: ['pendingEmergencyActions', chain] })
304      },
305    })
306  
307    const executeMutation = useMutation({
308      mutationFn: () => emergencyService.executeEmergencyAction(action.id, chain),
309      onSuccess: () => {
310        queryClient.invalidateQueries({ queryKey: ['pendingEmergencyActions', chain] })
311      },
312    })
313  
314    return (
315      <div className="p-4">
316        <div className="flex items-start justify-between">
317          <div>
318            <div className="flex items-center gap-2">
319              <span
320                className={`text-xs px-2 py-0.5 rounded ${
321                  actionInfo.severity === 'critical'
322                    ? 'bg-red-100 text-red-700'
323                    : actionInfo.severity === 'high'
324                    ? 'bg-orange-100 text-orange-700'
325                    : 'bg-yellow-100 text-yellow-700'
326                }`}
327              >
328                {actionInfo.severity}
329              </span>
330              <span className="font-medium">{actionInfo.label}</span>
331            </div>
332            <p className="text-sm text-gray-600 mt-1">{action.reason}</p>
333          </div>
334  
335          <div className="text-right">
336            <div className="text-sm">
337              {action.signatures.length} / {action.requiredSignatures} signatures
338            </div>
339            {status.timeRemaining > 0 && (
340              <div className="text-xs text-gray-500">
341                {formatTimeRemaining(status.timeRemaining)} remaining
342              </div>
343            )}
344          </div>
345        </div>
346  
347        {/* Signature progress */}
348        <div className="mt-3">
349          <div className="h-2 bg-gray-100 rounded-full overflow-hidden">
350            <div
351              className="h-full bg-red-500 transition-all"
352              style={{
353                width: `${(action.signatures.length / action.requiredSignatures) * 100}%`,
354              }}
355            />
356          </div>
357        </div>
358  
359        {/* Actions */}
360        <div className="mt-3 flex items-center gap-2">
361          {status.canSign && (
362            <button
363              onClick={() => signMutation.mutate()}
364              disabled={signMutation.isPending}
365              className="px-3 py-1.5 bg-red-600 text-white rounded text-sm disabled:opacity-50 hover:bg-red-700"
366            >
367              {signMutation.isPending ? 'Signing...' : 'Sign'}
368            </button>
369          )}
370  
371          {status.canExecute && (
372            <button
373              onClick={() => executeMutation.mutate()}
374              disabled={executeMutation.isPending}
375              className="px-3 py-1.5 bg-green-600 text-white rounded text-sm disabled:opacity-50 hover:bg-green-700"
376            >
377              {executeMutation.isPending ? 'Executing...' : 'Execute'}
378            </button>
379          )}
380  
381          {status.isExpired && (
382            <span className="text-sm text-red-600">Expired</span>
383          )}
384        </div>
385      </div>
386    )
387  }
388  
389  function formatTimeRemaining(seconds: number): string {
390    const hours = Math.floor(seconds / 3600)
391    const minutes = Math.floor((seconds % 3600) / 60)
392  
393    if (hours > 0) {
394      return `${hours}h ${minutes}m`
395    }
396    return `${minutes}m`
397  }
398  
399  function WarningIcon({ className }: { className?: string }) {
400    return (
401      <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
402        <path
403          strokeLinecap="round"
404          strokeLinejoin="round"
405          strokeWidth={2}
406          d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
407        />
408      </svg>
409    )
410  }
411  
412  function CheckIcon({ className }: { className?: string }) {
413    return (
414      <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
415        <path
416          strokeLinecap="round"
417          strokeLinejoin="round"
418          strokeWidth={2}
419          d="M5 13l4 4L19 7"
420        />
421      </svg>
422    )
423  }
424  
425  function LockIcon({ className }: { className?: string }) {
426    return (
427      <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
428        <path
429          strokeLinecap="round"
430          strokeLinejoin="round"
431          strokeWidth={2}
432          d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
433        />
434      </svg>
435    )
436  }