"""Direct SFTP sync + native rebuild (when Gitea push fails).""" from __future__ import annotations import sys import time from pathlib import Path import paramiko sys.stdout.reconfigure(encoding="utf-8", errors="replace") HOST = "107.173.30.245" USER = "root" PASS = "Xiao4669805" REMOTE = "/opt/nomadweb" DOMAIN = "nomadweb.nomadro.com" # Override via env or edit before run FILES = [ "README.md", ".gitignore", "frontend/src/app/changelog/page.tsx", "frontend/src/app/globals.css", "frontend/src/app/profile/page.tsx", "frontend/src/app/plan/page.tsx", "frontend/src/app/compare/page.tsx", "frontend/src/app/layout.tsx", "frontend/src/app/manifest.ts", "frontend/src/app/login/page.tsx", "frontend/src/app/sitemap.ts", "frontend/src/app/tools/page.tsx", "frontend/src/app/destinations/[slug]/page.tsx", "frontend/next.config.ts", "frontend/public/sw.js", "frontend/public/icons/icon.svg", "frontend/public/icons/icon-192.png", "frontend/public/icons/icon-512.png", "frontend/public/icons/apple-touch-icon.png", "frontend/src/components/DestinationDetailClient.tsx", "frontend/src/components/DestinationMatcher.tsx", "frontend/src/components/CompareModal.tsx", "frontend/src/components/CompareClient.tsx", "frontend/src/components/GlobalSearch.tsx", "frontend/src/components/HomeClient.tsx", "frontend/src/components/KeyboardShortcuts.tsx", "frontend/src/components/Navbar.tsx", "frontend/src/components/TripPlanner.tsx", "frontend/src/components/WorldMap.tsx", "frontend/src/components/Destinations.tsx", "frontend/src/components/MovePlanClient.tsx", "frontend/src/components/MatcherTrigger.tsx", "frontend/src/components/ParticleCanvas.tsx", "frontend/src/components/Loader.tsx", "frontend/src/components/WhenVisible.tsx", "frontend/src/components/Hero.tsx", "frontend/src/components/ToolsStrip.tsx", "frontend/src/components/JourneyStrip.tsx", "frontend/src/components/SectionNav.tsx", "frontend/src/components/Footer.tsx", "frontend/src/components/OnboardingTour.tsx", "frontend/src/components/PwaRegister.tsx", "frontend/src/components/BlogSection.tsx", "frontend/src/components/FAQSection.tsx", "frontend/src/components/VisaSection.tsx", "frontend/src/lib/tools.ts", "frontend/src/lib/types.ts", "frontend/src/lib/planMeta.ts", "frontend/src/lib/compareScore.ts", "frontend/src/lib/tripActions.ts", "frontend/src/lib/matcherPrefs.ts", "frontend/src/lib/tripStorage.ts", "frontend/src/lib/authNext.ts", "frontend/src/lib/planSync.ts", "frontend/src/lib/planIcs.ts", "frontend/src/lib/auth.tsx", "frontend/src/lib/api.ts", "frontend/src/lib/i18n/types.ts", "frontend/src/lib/i18n/dictionaries.ts", "frontend/src/lib/i18n/I18nProvider.tsx", "frontend/src/lib/i18n/index.ts", "frontend/package.json", "frontend/package-lock.json", "frontend/postcss.config.mjs", "frontend/content/book/book.json", "frontend/content/book/en/book.json", "frontend/content/book/features.json", "frontend/content/book/site.json", "frontend/content/book/chapters/00-preface.md", "frontend/content/book/chapters/01-first-remote.md", "frontend/content/book/chapters/02-chiangmai.md", "frontend/content/book/chapters/03-gear.md", "frontend/content/book/chapters/04-homelab.md", "frontend/content/book/chapters/05-network.md", "frontend/content/book/chapters/06-epilogue.md", "frontend/src/app/book/layout.tsx", "frontend/src/app/book/loading.tsx", "frontend/src/app/book/page.tsx", "frontend/src/app/book/ebook-globals.css", "frontend/src/app/book/opengraph-image.tsx", "frontend/src/app/book/read/page.tsx", "frontend/src/app/book/read/[slug]/page.tsx", "frontend/src/components/ebook/AdminConsole.tsx", "frontend/src/components/ebook/AdSlot.tsx", "frontend/src/components/ebook/AnalyticsProvider.tsx", "frontend/src/components/ebook/AnimatedCounter.tsx", "frontend/src/components/ebook/BookEnd.tsx", "frontend/src/components/ebook/BookmarkButton.tsx", "frontend/src/components/ebook/BookSearch.tsx", "frontend/src/components/ebook/BuyEbook.tsx", "frontend/src/components/ebook/ChapterHero.tsx", "frontend/src/components/ebook/ChapterMenu.tsx", "frontend/src/components/ebook/ChapterNav.tsx", "frontend/src/components/ebook/ChapterReader.tsx", "frontend/src/components/ebook/ChapterSidebar.tsx", "frontend/src/components/ebook/ContinueReading.tsx", "frontend/src/components/ebook/ContinuousReader.tsx", "frontend/src/components/ebook/CoverChapterGrid.tsx", "frontend/src/components/ebook/CoverPage.tsx", "frontend/src/components/ebook/CryptoSupport.tsx", "frontend/src/components/ebook/FeedbackPanel.tsx", "frontend/src/components/ebook/FocusReader.tsx", "frontend/src/components/ebook/FontSizeControl.tsx", "frontend/src/components/ebook/GrowthKit.tsx", "frontend/src/components/ebook/HighlightPainter.tsx", "frontend/src/components/ebook/InChapterSearch.tsx", "frontend/src/components/ebook/InsightCarousel.tsx", "frontend/src/components/ebook/KeyboardNav.tsx", "frontend/src/components/ebook/LocaleProvider.tsx", "frontend/src/components/ebook/LocaleSwitcher.tsx", "frontend/src/components/ebook/MarkdownContent.tsx", "frontend/src/components/ebook/MobileChapterBar.tsx", "frontend/src/components/ebook/ProgressBar.tsx", "frontend/src/components/ebook/ProgressRing.tsx", "frontend/src/components/ebook/Providers.tsx", "frontend/src/components/ebook/ReaderLayout.tsx", "frontend/src/components/ebook/ReaderSettings.tsx", "frontend/src/components/ebook/ReaderShare.tsx", "frontend/src/components/ebook/ReaderShortcuts.tsx", "frontend/src/components/ebook/ReadingModeToggle.tsx", "frontend/src/components/ebook/ReadingTracker.tsx", "frontend/src/components/ebook/ScrollReaderShell.tsx", "frontend/src/components/ebook/ScrollReadingTracker.tsx", "frontend/src/components/ebook/ScrollReveal.tsx", "frontend/src/components/ebook/ScrollToTop.tsx", "frontend/src/components/ebook/SelectionShare.tsx", "frontend/src/components/ebook/ShareButton.tsx", "frontend/src/components/ebook/ShareQuoteCard.tsx", "frontend/src/components/ebook/ShareToX.tsx", "frontend/src/components/ebook/SkillRadar.tsx", "frontend/src/components/ebook/SupportThanksContent.tsx", "frontend/src/components/ebook/ThemeToggle.tsx", "frontend/src/lib/ebook/affiliate.ts", "frontend/src/lib/ebook/analytics.ts", "frontend/src/lib/ebook/book-types.ts", "frontend/src/lib/ebook/book.server.ts", "frontend/src/lib/ebook/contentPath.ts", "frontend/src/lib/ebook/bookmarks.ts", "frontend/src/lib/ebook/creem.ts", "frontend/src/lib/ebook/features.server.ts", "frontend/src/lib/ebook/features.ts", "frontend/src/lib/ebook/headings.ts", "frontend/src/lib/ebook/highlights.ts", "frontend/src/lib/ebook/i18n/index.ts", "frontend/src/lib/ebook/i18n/messages/en.ts", "frontend/src/lib/ebook/i18n/messages/zh.ts", "frontend/src/lib/ebook/i18n/server.ts", "frontend/src/lib/ebook/i18n/types.ts", "frontend/src/lib/ebook/metadata.ts", "frontend/src/lib/ebook/nowpayments.ts", "frontend/src/lib/ebook/paths.ts", "frontend/src/lib/ebook/reader-prefs.ts", "frontend/src/lib/ebook/reading-progress.ts", "frontend/src/lib/ebook/search.ts", "frontend/src/lib/ebook/share-text.ts", "frontend/src/lib/ebook/share.ts", "frontend/src/lib/ebook/site-config.server.ts", "frontend/src/lib/ebook/site-env.ts", "frontend/src/lib/ebook/site-types.ts", "frontend/src/lib/ebook/site.ts", "frontend/src/lib/ebook/support-tiers.ts", "frontend/src/lib/ebook/time.ts", "frontend/src/lib/ebook/utm.ts", "backend/app/services/auth.py", "backend/app/routers/auth.py", "backend/app/schemas.py", "backend/app/main.py", "backend/app/routers/api.py", "backend/app/routers/payment.py", "backend/app/routers/social.py", "backend/app/routers/community.py", "backend/app/data/community_data.py", "backend/app/data/digital_content.py", "backend/app/data/social_profiles.py", "backend/app/services/meetup_live.py", "backend/app/services/payment_service.py", "backend/app/services/recommendations.py", "backend/app/services/social_store.py", "backend/app/services/community_store.py", "backend/app/services/payment_providers.py", "frontend/src/app/next-stop/page.tsx", "frontend/src/app/meetups/page.tsx", "frontend/src/app/meetups/[id]/live/page.tsx", "frontend/src/app/community/page.tsx", "frontend/src/app/community/new/page.tsx", "frontend/src/app/community/[id]/page.tsx", "frontend/src/app/gigs/page.tsx", "frontend/src/app/pricing/page.tsx", "frontend/src/app/notifications/page.tsx", "frontend/src/app/members/[id]/page.tsx", "frontend/src/app/meetups/host/page.tsx", "frontend/src/app/dating/likes/page.tsx", "frontend/src/app/tools/[id]/page.tsx", "frontend/src/app/dating/page.tsx", "frontend/src/app/chat/page.tsx", "frontend/src/app/chat/[id]/page.tsx", "frontend/src/app/join/page.tsx", "frontend/src/app/join/paid/page.tsx", "frontend/src/app/digital/page.tsx", "frontend/src/app/digital/course/page.tsx", "frontend/src/app/digital/course/[m]/[l]/page.tsx", "frontend/src/app/digital/jobs/page.tsx", "frontend/src/components/NextStopClient.tsx", "frontend/src/components/MeetupsClient.tsx", "frontend/src/components/MeetupLiveClient.tsx", "frontend/src/components/CommunityHubClient.tsx", "frontend/src/components/CommunityNewClient.tsx", "frontend/src/components/DiscussionDetailClient.tsx", "frontend/src/components/MeetupsHostClient.tsx", "frontend/src/components/GigsClient.tsx", "frontend/src/components/PricingClient.tsx", "frontend/src/components/NotificationsClient.tsx", "frontend/src/components/MemberProfileClient.tsx", "frontend/src/components/DatingLikesClient.tsx", "frontend/src/components/SiteShell.tsx", "frontend/src/components/ToolRunnerClient.tsx", "frontend/src/components/DatingClient.tsx", "frontend/src/components/ChatClient.tsx", "frontend/src/components/ChatThreadClient.tsx", "frontend/src/components/JoinClient.tsx", "frontend/src/components/JoinPaidClient.tsx", "frontend/src/components/DigitalHomeClient.tsx", "frontend/src/components/DigitalCourseClient.tsx", "frontend/src/components/DigitalLessonClient.tsx", "frontend/src/components/DigitalJobsClient.tsx", "frontend/src/lib/toolsRegistry.tsx", ] BUILD = f""" set -euo pipefail exec 9>/var/lock/nomadro-deploy.lock if ! flock -n 9; then echo LOCK_BUSY exit 75 fi REMOTE={REMOTE} DOMAIN={DOMAIN} export NEXT_TELEMETRY_DISABLED=1 export NEXT_PUBLIC_API_URL=https://${{DOMAIN}}/api/v1 export NEXT_PUBLIC_SITE_URL=https://${{DOMAIN}} export NODE_ENV=production echo RESTART_API systemctl restart nomadro-api echo BUILD_START cd "$REMOTE/frontend" rm -rf .next NPM_CONFIG_PRODUCTION=false npm install npm run build mkdir -p .next/standalone/.next rm -rf .next/standalone/public .next/standalone/.next/static cp -a public .next/standalone/public cp -a .next/static .next/standalone/.next/static rm -rf .next/standalone/content mkdir -p .next/standalone/content cp -a content .next/standalone/content # also keep content next to process cwd variants rm -rf .next/standalone/frontend 2>/dev/null || true systemctl restart nomadro-web ok=0 for i in $(seq 1 40); do web=$(curl -s -o /dev/null -w '%{{http_code}}' http://127.0.0.1:3055/ || true) api=$(curl -s http://127.0.0.1:8055/api/v1/health || true) echo "try=$i web=$web api=$api" if echo "$api" | grep -q '"status":"ok"' && [ "$web" = "200" ]; then ok=1 break fi sleep 1 done curl -skI "https://${{DOMAIN}}/plan" | head -6 curl -sk "https://${{DOMAIN}}/api/v1/health" echo DIRECT_SYNC=1 [ "$ok" = 1 ] """ def ensure_dir(sftp: paramiko.SFTPClient, path: str) -> None: parts = path.strip("/").split("/") cur = "" for p in parts: cur += "/" + p try: sftp.stat(cur) except FileNotFoundError: try: sftp.mkdir(cur) except OSError: pass def main() -> int: root = Path(__file__).resolve().parents[1] t0 = time.time() client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) print("Connecting...") client.connect(HOST, username=USER, password=PASS, timeout=30) sftp = client.open_sftp() for rel in FILES: local = root / rel if not local.exists(): print(f"SKIP missing {rel}") continue remote = f"{REMOTE}/{rel}" print(f"UPLOAD {rel}") ensure_dir(sftp, str(Path(remote).parent).replace("\\", "/")) sftp.put(str(local), remote) sftp.close() print("Uploaded. Building...") _, stdout, stderr = client.exec_command( f"cat > /tmp/nomadro-direct.sh <<'EOF'\n{BUILD}\nEOF\nbash /tmp/nomadro-direct.sh", timeout=900, ) while True: line = stdout.readline() if not line: break print(line, end="") err = stderr.read().decode("utf-8", errors="replace") code = stdout.channel.recv_exit_status() if err.strip(): print("LOG:", err[-4000:]) print("exit:", code) client.close() print(f"Done in {int(time.time() - t0)}s") return code if __name__ == "__main__": raise SystemExit(main())