-
Notifications
You must be signed in to change notification settings - Fork 0
Phase 7 #12
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
Phase 7 #12
Changes from all commits
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 |
|---|---|---|
|
|
@@ -13,9 +13,15 @@ | |
| from arq import create_pool | ||
| from ..config import settings | ||
| from arq.connections import RedisSettings | ||
| import logging | ||
|
|
||
| router = APIRouter(prefix="/resources", tags=["resources"]) | ||
|
|
||
| # Configure logging for the task | ||
| logging.basicConfig(level=logging.INFO) | ||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| @router.post("/", response_model=ResourceOut) | ||
| async def upload_resource( | ||
| type: str = Form(...), | ||
|
|
@@ -88,46 +94,58 @@ async def upload_resource( | |
| detail="Failed to upload file to storage" | ||
| ) | ||
|
|
||
| # Create DB record (not committed yet) | ||
| new_resource = Resource( | ||
| user_id=current_user.id, | ||
| filename=file.filename, | ||
| file_url=file_url, | ||
| type=type, | ||
| status="processing" | ||
| ) | ||
| db.add(new_resource) | ||
| await db.flush() # Flush to get the ID but don't commit | ||
|
|
||
| from ..models.job import Job | ||
| new_job = Job( | ||
| user_id=current_user.id, | ||
| job_type="ingest", | ||
| status="queued", | ||
| ref_id=new_resource.id | ||
| ) | ||
| db.add(new_job) | ||
| await db.flush() | ||
|
|
||
| # Enqueue background extraction task before committing DB | ||
| try: | ||
| redis = await create_pool(RedisSettings.from_dsn(settings.REDIS_URL)) | ||
| # Pass job_id as second argument | ||
| await redis.enqueue_job('extraction_task', str(new_resource.id), str(new_job.id)) | ||
| # Create DB record (not committed yet) | ||
| new_resource = Resource( | ||
| user_id=current_user.id, | ||
| filename=file.filename, | ||
| file_url=file_url, | ||
| type=type, | ||
| status="processing" | ||
| ) | ||
| db.add(new_resource) | ||
| await db.flush() # Flush to get the ID | ||
|
|
||
| # Only commit if enqueue was successful | ||
| await db.commit() | ||
| await db.refresh(new_resource) | ||
| from ..models.job import Job | ||
| new_job = Job( | ||
| user_id=current_user.id, | ||
| job_type="ingest", | ||
| status="queued", | ||
| ref_id=new_resource.id | ||
| ) | ||
| db.add(new_job) | ||
| await db.flush() | ||
|
|
||
| # Enqueue background extraction task | ||
| redis = await create_pool(RedisSettings.from_dsn(settings.REDIS_URL)) | ||
| try: | ||
| # Use _job_id to make it easy to find in hooks | ||
| await redis.enqueue_job( | ||
| 'extraction_task', | ||
| str(new_resource.id), | ||
| str(new_job.id), | ||
| _job_id=str(new_job.id) | ||
| ) | ||
| await db.commit() | ||
| await db.refresh(new_resource) | ||
| except Exception as e: | ||
| await db.rollback() | ||
| raise e # Caught by outer try/except | ||
| finally: | ||
| if 'redis' in locals(): | ||
| await redis.close() | ||
|
|
||
| except Exception as e: | ||
| await db.rollback() | ||
| # Should also ideally delete the file from Spaces here if we were strict | ||
| # CLEANUP: Delete from storage if DB transaction failed | ||
| logger.error(f"Upload transaction failed, cleaning up storage: {e}") | ||
| storage_service.delete_file(object_name) | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| if isinstance(e, HTTPException): | ||
| raise e | ||
| raise HTTPException( | ||
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | ||
| detail=f"Failed to queue background task: {str(e)}" | ||
| detail=f"Failed to complete upload transaction: {str(e)}" | ||
| ) | ||
|
Comment on lines
145
to
148
Contributor
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. Avoid returning raw internal exception text to clients. Line 141 exposes internal error details ( Suggested fix raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=f"Failed to complete upload transaction: {str(e)}"
+ detail="Failed to complete upload transaction."
)🧰 Tools🪛 Ruff (0.15.13)[warning] 139-142: Within an (B904) [warning] 141-141: Use explicit conversion flag Replace with conversion flag (RUF010) 🤖 Prompt for AI Agents |
||
| finally: | ||
| if 'redis' in locals(): | ||
| await redis.close() | ||
|
|
||
| return new_resource | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import { Navigate } from 'react-router-dom'; | ||
| import { useAuthStore } from '../store/authStore'; | ||
|
|
||
| export default function PublicRoute({ children }: { children: React.ReactNode }) { | ||
| const token = useAuthStore((state) => state.token); | ||
|
|
||
| if (token) { | ||
| return <Navigate to="/" replace />; | ||
| } | ||
|
|
||
| return <>{children}</>; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| import { | ||
| Dialog, | ||
| DialogContent, | ||
| DialogDescription, | ||
| DialogHeader, | ||
| DialogTitle, | ||
| DialogFooter, | ||
| } from "./ui/dialog"; | ||
| import { Button } from "./ui/button"; | ||
| import { CheckCircle2, Zap, FileText, FileEdit, Crown } from "lucide-react"; | ||
|
|
||
| interface UpgradeModalProps { | ||
| isOpen: boolean; | ||
| onClose: () => void; | ||
| message?: string; | ||
| } | ||
|
|
||
| export function UpgradeModal({ isOpen, onClose, message }: UpgradeModalProps) { | ||
| const benefits = [ | ||
| { | ||
| icon: <Zap className="size-4 text-yellow-500" />, | ||
| text: "Unlimited AI Tutor Questions" | ||
| }, | ||
| { | ||
| icon: <FileEdit className="size-4 text-purple-500" />, | ||
| text: "Unlimited Mock Exam Generations" | ||
| }, | ||
| { | ||
| icon: <FileText className="size-4 text-blue-500" />, | ||
| text: "Unlimited Study Resource Storage" | ||
| }, | ||
| { | ||
| icon: <Crown className="size-4 text-primary" />, | ||
| text: "Priority AI Model Access" | ||
| } | ||
| ]; | ||
|
|
||
| return ( | ||
| <Dialog open={isOpen} onOpenChange={onClose}> | ||
| <DialogContent className="sm:max-w-[450px] overflow-hidden border-primary/20"> | ||
| <div className="absolute top-0 left-0 w-full h-1.5 bg-primary" /> | ||
|
|
||
| <DialogHeader className="pt-4"> | ||
| <div className="size-12 bg-primary/10 rounded-full flex items-center justify-center mb-4"> | ||
| <Crown className="size-6 text-primary" /> | ||
| </div> | ||
| <DialogTitle className="text-2xl font-black tracking-tight">Upgrade to Premium</DialogTitle> | ||
| <DialogDescription className="text-base pt-2"> | ||
| {message || "You've reached your free tier limit. Upgrade now to unlock the full power of PYQ Gen."} | ||
| </DialogDescription> | ||
| </DialogHeader> | ||
|
|
||
| <div className="py-6 space-y-4"> | ||
| <h4 className="text-sm font-bold uppercase tracking-widest text-muted-foreground">Premium Benefits:</h4> | ||
| <div className="grid grid-cols-1 gap-3"> | ||
| {benefits.map((benefit, i) => ( | ||
| <div key={i} className="flex items-center gap-3 p-3 rounded-xl bg-muted/30 border border-border/50"> | ||
| <div className="shrink-0">{benefit.icon}</div> | ||
| <span className="text-sm font-medium text-foreground">{benefit.text}</span> | ||
| <CheckCircle2 className="size-4 text-primary ml-auto" /> | ||
| </div> | ||
| ))} | ||
| </div> | ||
| </div> | ||
|
|
||
| <DialogFooter className="flex-col sm:flex-col gap-3"> | ||
| <Button | ||
| className="w-full h-12 text-base font-bold shadow-lg shadow-primary/20" | ||
| onClick={() => { | ||
| // Future: Redirect to checkout or open Razorpay | ||
| console.log("Redirecting to payment..."); | ||
| onClose(); | ||
| }} | ||
| > | ||
| Upgrade Now — $9/mo | ||
| </Button> | ||
|
Comment on lines
+67
to
+76
Contributor
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. Wire the primary CTA to an actual upgrade action. Line 71 only logs and closes the dialog, so “Upgrade Now” does not upgrade or redirect users. 💡 Suggested fix interface UpgradeModalProps {
isOpen: boolean;
onClose: () => void;
+ onUpgrade: () => void;
message?: string;
}
-export function UpgradeModal({ isOpen, onClose, message }: UpgradeModalProps) {
+export function UpgradeModal({ isOpen, onClose, onUpgrade, message }: UpgradeModalProps) {
@@
<Button
className="w-full h-12 text-base font-bold shadow-lg shadow-primary/20"
onClick={() => {
- // Future: Redirect to checkout or open Razorpay
- console.log("Redirecting to payment...");
- onClose();
+ onUpgrade();
}}
>
Upgrade Now — $9/mo
</Button>🤖 Prompt for AI Agents |
||
| <Button variant="ghost" onClick={onClose} className="w-full font-semibold"> | ||
| Maybe Later | ||
| </Button> | ||
| </DialogFooter> | ||
| </DialogContent> | ||
| </Dialog> | ||
| ); | ||
| } | ||
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.
Don’t delete storage files after a successful DB commit.
If Line 123 commit succeeds but Line 124 refresh fails, current logic still hits cleanup at Line 135 and deletes the uploaded file, leaving a committed DB record pointing to missing storage.
Suggested fix
Also applies to: 133-136
🤖 Prompt for AI Agents