A simple tool which lets you scrape twitter accounts and crosspost them to bluesky accounts! Comes with a CLI and a webapp for managing profiles! Works with images/videos/link embeds/threads.
13
fork

Configure Feed

Select the types of activity you want to include in your feed.

Fix dashboard queue actions and status sync logic

jack 420c9410 26c9ab73

+125 -3
+116 -1
web/src/App.tsx
··· 754 754 const noticeTimerRef = useRef<number | null>(null); 755 755 const importInputRef = useRef<HTMLInputElement>(null); 756 756 const postsSearchRequestRef = useRef(0); 757 + const statusRequestRef = useRef(0); 758 + const statusMutationRef = useRef(0); 757 759 758 760 const isAdmin = me?.isAdmin ?? false; 759 761 const effectivePermissions = useMemo<UserPermissions>(() => normalizePermissions(me?.permissions), [me?.permissions]); ··· 840 842 return; 841 843 } 842 844 845 + const requestToken = ++statusRequestRef.current; 846 + const mutationTokenAtStart = statusMutationRef.current; 847 + 843 848 try { 844 849 const response = await axios.get<StatusResponse>('/api/status', { headers: authHeaders }); 850 + if (requestToken !== statusRequestRef.current) { 851 + return; 852 + } 853 + if (mutationTokenAtStart !== statusMutationRef.current) { 854 + return; 855 + } 845 856 setStatus(response.data); 846 857 } catch (error) { 847 858 handleAuthFailure(error, 'Failed to fetch status.'); ··· 1635 1646 } 1636 1647 1637 1648 try { 1649 + statusMutationRef.current += 1; 1650 + setStatus((previous) => 1651 + previous 1652 + ? { 1653 + ...previous, 1654 + nextCheckTime: Date.now() + 1000, 1655 + currentStatus: { 1656 + ...previous.currentStatus, 1657 + state: 'checking', 1658 + message: 'Manual run requested...', 1659 + lastUpdate: Date.now(), 1660 + backfillMappingId: undefined, 1661 + backfillRequestId: undefined, 1662 + }, 1663 + } 1664 + : previous, 1665 + ); 1638 1666 await axios.post('/api/run-now', {}, { headers: authHeaders }); 1639 1667 showNotice('info', 'Check triggered.'); 1640 1668 await fetchStatus(); ··· 1654 1682 } 1655 1683 1656 1684 try { 1685 + statusMutationRef.current += 1; 1686 + setStatus((previous) => 1687 + previous 1688 + ? { 1689 + ...previous, 1690 + pendingBackfills: [], 1691 + currentStatus: { 1692 + ...previous.currentStatus, 1693 + state: 'idle', 1694 + message: 'Backfill queue cleared', 1695 + backfillMappingId: undefined, 1696 + backfillRequestId: undefined, 1697 + lastUpdate: Date.now(), 1698 + }, 1699 + } 1700 + : previous, 1701 + ); 1657 1702 await axios.post('/api/backfill/clear-all', {}, { headers: authHeaders }); 1658 1703 showNotice('success', 'Backfill queue cleared.'); 1659 1704 await fetchStatus(); ··· 1662 1707 } 1663 1708 }; 1664 1709 1710 + const cancelQueuedBackfill = async (mappingId: string) => { 1711 + if (!authHeaders) { 1712 + return; 1713 + } 1714 + 1715 + const mapping = mappings.find((entry) => entry.id === mappingId); 1716 + if (!mapping || !canManageMapping(mapping)) { 1717 + showNotice('error', 'You do not have permission to cancel this queue entry.'); 1718 + return; 1719 + } 1720 + 1721 + try { 1722 + statusMutationRef.current += 1; 1723 + setStatus((previous) => { 1724 + if (!previous) { 1725 + return previous; 1726 + } 1727 + 1728 + const nextPending = previous.pendingBackfills 1729 + .filter((entry) => entry.id !== mappingId) 1730 + .map((entry, index) => ({ ...entry, position: index + 1 })); 1731 + return { 1732 + ...previous, 1733 + pendingBackfills: nextPending, 1734 + }; 1735 + }); 1736 + await axios.delete(`/api/backfill/${mappingId}`, { headers: authHeaders }); 1737 + showNotice('success', 'Queue entry cancelled.'); 1738 + await fetchStatus(); 1739 + } catch (error) { 1740 + handleAuthFailure(error, 'Failed to cancel queue entry.'); 1741 + } 1742 + }; 1743 + 1665 1744 const requestBackfill = async (mappingId: string, mode: 'normal' | 'reset') => { 1666 1745 if (!authHeaders) { 1667 1746 return; ··· 1692 1771 await axios.delete(`/api/mappings/${mappingId}/cache`, { headers: authHeaders }); 1693 1772 } 1694 1773 1774 + statusMutationRef.current += 1; 1775 + setStatus((previous) => { 1776 + if (!previous) { 1777 + return previous; 1778 + } 1779 + 1780 + const now = Date.now(); 1781 + const existing = previous.pendingBackfills.filter((entry) => entry.id !== mappingId); 1782 + const nextPending = [ 1783 + ...existing, 1784 + { 1785 + id: mappingId, 1786 + limit: safeLimit, 1787 + queuedAt: now, 1788 + sequence: now, 1789 + requestId: `ui-${now}`, 1790 + position: existing.length + 1, 1791 + }, 1792 + ].map((entry, index) => ({ ...entry, position: index + 1 })); 1793 + 1794 + return { 1795 + ...previous, 1796 + pendingBackfills: nextPending, 1797 + }; 1798 + }); 1695 1799 await axios.post(`/api/backfill/${mappingId}`, { limit: safeLimit }, { headers: authHeaders }); 1696 1800 showNotice( 1697 1801 'success', ··· 3094 3198 void requestBackfill(mapping.id, 'normal'); 3095 3199 }} 3096 3200 > 3097 - Backfill 3201 + Add to queue 3098 3202 </Button> 3203 + {queued && !active ? ( 3204 + <Button 3205 + variant="ghost" 3206 + size="sm" 3207 + onClick={() => { 3208 + void cancelQueuedBackfill(mapping.id); 3209 + }} 3210 + > 3211 + Cancel queue 3212 + </Button> 3213 + ) : null} 3099 3214 {isAdmin ? ( 3100 3215 <Button 3101 3216 variant="subtle"
+9 -2
web/src/components/ui/button.tsx
··· 32 32 VariantProps<typeof buttonVariants> {} 33 33 34 34 const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( 35 - ({ className, variant, size, ...props }, ref) => { 36 - return <button className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />; 35 + ({ className, variant, size, type, ...props }, ref) => { 36 + return ( 37 + <button 38 + className={cn(buttonVariants({ variant, size, className }))} 39 + ref={ref} 40 + type={type ?? 'button'} 41 + {...props} 42 + /> 43 + ); 37 44 }, 38 45 ); 39 46 Button.displayName = 'Button';