AI 资讯
One Checkbox, Three Kinds of State in a Chrome MV3 Extension
I thought I had a settings bug. What I actually had was three different kinds of state pretending to be one boolean. While building a Chrome Manifest V3 email-tracker blocker, I expected a simple flow: you flip Gmail on in the settings, and the extension starts working in Gmail. That was the theory, anyway. The problem showed up when I was testing on a second Chrome profile. I'd enabled Gmail on my main profile, and Chrome Sync helpfully carried that preference over to the other one. But the optional permission for mail.google.com didn't come along — host grants live in the local profile and never sync. Profile number two now believed Gmail was enabled while lacking the host grant needed to inject the inbox content script or inspect its DOM. Depending on how you write your code, that's either a silent no-op or an extension quietly behaving as if access exists when it does not. Neither is great. Once I stopped and wrote it down, the picture got clearer. There are three separate things here: the inbox the user wants enabled, the host access Chrome has actually granted in this profile, and the dynamic DNR rules that are currently installed . Collapsing them into one flag is convenient. It's also wrong. The manifest is a menu, not an order The extension declares each webmail origin under optional_host_permissions . Every inbox gets activated on its own, and Chrome only asks the user for access when they turn that particular integration on. Here's the thing I had to internalize: declaring an optional origin means nothing by itself. Until the live grant exists, the extension has no business registering a content script for that inbox, poking at its DOM, or — by its own scoping policy — activating client-scoped blocking rules for it. Why bother with per-inbox prompts at all? Mostly trust. A tracker blocker that asks for all your webmail up front looks exactly like the thing it's supposed to protect you from. Asking for Gmail when you enable Gmail — and nothing more — is an
安全
Stop using the localStorage hack to sync browser tabs. BroadcastChannel does it natively.
When a user logs out in one tab, the other tabs should follow. When they update their cart, every...
AI 资讯
I built OneToolBox — free browser-based tools for developers
Hey devs👋 I've been building OneToolBox : https://onetoolbox.dev/ It's a collection of free web utilities for developers and creators — JSON tools, YAML validation, hash generation, text diffing, image tools, converters, and more. The main idea is simple: do as much as possible directly in the browser, without requiring accounts or uploading users' files/data to a server. I'm still actively improving it, and I'd really appreciate feedback from developers here. What would you improve? Which tools are missing? Are there tools you use regularly that you'd like to see added? Any UX problems or annoying workflows? Is there anything you'd change about the interface? Are there performance, privacy, or technical improvements you'd recommend? I'd especially appreciate criticism from people who actually use developer utilities regularly. Don't hesitate to point out what's bad or unnecessary — that's more useful to me than compliments. If you have a minute, take a look and tell me what you'd change. Thanks! 🙏
AI 资讯
I Built a Crypto-Native Craigslist with Manual Escrow — Here's Why and How
The Problem There are millions of people holding crypto who want to spend it on real things — hire a developer, buy a script, sell design work. But where do they go? Telegram OTC chats → chaotic, no protection, scam-heavy Forum classifieds → threads get buried in hours P2P exchange sections → designed for fiat conversion, not commerce I decided to build a dedicated marketplace for this. What I Built CryptoBoard — a classifieds platform with Web3 wallet authentication. 🔗 https://crypto.my-board.org/ Tech decisions: Auth : Wallet-only (MetaMask, Trust Wallet, WalletConnect). No backend user database with emails and passwords to get hacked. Listings : Icon-based instead of user-uploaded images. Keeps the UI clean and avoids the "flea market" look. Messaging : Built-in chat between buyers and sellers. Escrow : This is the interesting part (see below). The Escrow Problem with Digital Goods Traditional escrow works like this: Buyer sends money to escrow Seller delivers product Buyer confirms → escrow releases money But with digital goods (source code, design files), step 3 is broken: The buyer can receive the files, say "this isn't what I wanted," request a refund, and keep a copy The seller has no recourse The escrow service has no way to verify the claim My Solution: Human-Powered Escrow Instead of just holding funds, the platform admin becomes an active verifier: Seller sends product + testing instructions to admin Admin installs/runs the product on their own machine Admin performs agreed-upon tests and records a screencast Buyer watches the screencast — verified by a neutral party, not the seller If satisfied, buyer sends crypto directly to seller Admin verifies the on-chain transaction Admin delivers files to buyer Admin deletes all copies (per agreement) Is it scalable? Probably not infinitely. But for high-value digital transactions ($100–$10,000+), having a human in the loop is actually a feature, not a bug. Design Philosophy I deliberately chose not to allow user
开发者
Free, Zero-Dependency YouTube Website Embed (Self-Hostable PHP/JS)
Hey DEV community! 👋 If you've ever tried to embed a dynamic YouTube channel feed, a live stream detector, or playlist carousel on a client site, you've probably run into two major issues: Expensive SaaS widgets that slap watermarks on your site unless you pay a monthly fee. Leaking your YouTube API Key directly in the frontend script. To solve this, we built YT Widget —a free, self-hostable, dependency-free JavaScript library that handles YouTube feeds, playlists, channel stats, and live stream status seamlessly. 📦 Where to Get It The project is fully open-source and ready for your production projects: Source Code & Contributions: scott8462 / YT-Widget A free, self-hostable, dependency-free JavaScript library for embedding YouTube feeds, playlists, channel stats, single videos, and live stream status on any website. YT Widget — Free Open-Source YouTube Website Embed A free, self-hostable, dependency-free JavaScript library for embedding YouTube feeds, playlists, channel stats, single videos, and live stream status on any website — just like SociableKIT, but 100% free and open-source. Created and provided free to the developer community by R&S Development . ✨ Features 📺 5 Widget Types feed : Latest channel uploads grid or list live : Auto-detects live broadcasts and embeds the live player — shows a custom Offline Card with recent uploads when offline playlist : Show videos from any YouTube playlist stats : Channel metrics cards (Subscribers, Views, Videos count) single : Responsive single video player with metadata 🔒 Secure PHP Server Proxy ( proxy/ ) : Keep your YouTube API key hidden server-side with built-in CORS, rate limiting, and 5-minute response caching. 🎨 Full Color Customization Light & Dark themes Custom Accent / Button… View on GitHub Alternative Downloads & Mirrors: Download on SourceForge ✨ Core Features 📺 5 Widget Types: Switch layouts instantly ( feed grid/list, live stream detector, playlist fetcher, profile stats , or single responsive video). 🔒 Se
AI 资讯
I Got the Internship Offer… and Then I Had to Say No.
A few days ago, I went for an internship opportunity that I was genuinely excited about. I had been looking forward to it for a long time. When I got the opportunity to attend a 3-day demo/trial period , I went in with a lot of excitement. I wanted to prove myself, learn as much as possible, and hopefully turn those three days into something bigger. And honestly, I gave it my best. I showed up, worked, learned, asked questions, and tried to contribute wherever I could. Then came the moment I had been hoping for. I received the offer letter. ❤️ For a moment, I was extremely happy. After being out of college and working hard to build my skills, finally getting an offer felt like a big step forward. But then I had to look at the practical side. The internship was work from office , and the stipend was ₹7,000/month . The biggest challenge was the distance. I live around 90 km away from the office. When I calculated the daily travel, food, and other expenses, I realized that accepting the internship would put a huge financial burden on me every month. And that was a very difficult realization. Because emotionally, I wanted to say: "Yes, I got an internship. Let's do this!" But practically, I had to say: "I can't afford this right now." So I rejected the offer. And honestly? It hurts. Not because the company did something wrong. Not because I didn't want to work. But because I finally got an opportunity I was excited about, gave it my best during the trial period, received the offer… and still had to walk away from it. I've been feeling pretty bad about it. There is always this thought in the back of my mind: "What if I had just accepted it?" But I'm also trying to remind myself that rejecting one opportunity doesn't mean I've failed. Sometimes an opportunity can be good and still not be right for your current situation. I'm taking this experience as a lesson: Getting an offer is not the final goal. Salary/stipend matters. Location and travel expenses matter. Your time ma
AI 资讯
I built an embeddable screen-time calculator that doesn't phone home
Most embeddable widgets are surveillance with rounded corners. You paste one script tag, it opens a socket back to someone else's server, drops analytics, fingerprints the page, and turns your article into their funnel. I wanted the opposite. I had built a small screen-time calculator for an iPhone side project. You enter daily phone hours, how much of that time you'd actually want back, and your age. It returns the number not just as hours per year, but as waking years of the life you have left . The surprising part was not the maths. The surprising part was that the calculator itself was the first marketing asset I had built that people might reasonably link to. So the next step was obvious: make it embeddable. Constraints I gave myself four rules: No tracking script No backend callback No cookie or storage requirement Useful standalone, but with a real reason to click through That ruled out the normal widget pattern immediately. I did not want a script that asks the host page for DOM access. I did not want the embed to send typed values back to me. And I did not want to bolt analytics onto a tool whose whole public claim is "nothing leaves your device". So the widget became a single static iframe page. The embed snippet This is the whole thing: <iframe src= "https://shantj.github.io/sproutguard/embed.html" width= "100%" height= "620" style= "border:0;max-width:600px" loading= "lazy" title= "Screen time calculator" ></iframe> <p style= "font-size:13px;opacity:.7;margin:6px 0 0" > <a href= "https://shantj.github.io/sproutguard/screen-time-calculator.html?ct=embed-credit" > Screen Time Calculator </a> — free, no signup, runs in your browser. </p> No JavaScript include. No SDK. No npm package. Just an iframe and a credit link. The iframe points at a page that contains the calculator UI and the arithmetic. Because it is a static page, the host site never has to trust my script with its DOM. The actual calculator logic The core number is intentionally boring: const LIF
开源项目
🔥 react / create-react-app - Set up a modern web app by running one command.
GitHub热门项目 | Set up a modern web app by running one command. | Stars: 103,304 | 32 stars this week | 语言: JavaScript
开源项目
🔥 lackeyjb / playwright-skill - Claude Code Skill for browser automation with Playwright. Mo
GitHub热门项目 | Claude Code Skill for browser automation with Playwright. Model-invoked - Claude autonomously writes and executes custom automation for testing and validation. | Stars: 2,994 | 3 stars today | 语言: JavaScript
AI 资讯
Building a Chrome Extension to Auto-Save Gemini Chat Logs using AI (Part 1)
This article was originally published on e-shikumi-labo . Hello, I'm Shin from e-Shikumi-Labo. How do you all manage your conversations with Gemini? When you manage to extract a useful response from the AI, have you ever thought, "I want to keep this somewhere"? It all started from a simple, practical desire in my daily work: "I want to automatically save useful conversations from Gemini to a spreadsheet before they fade away." So, borrowing the power of Generative AI (Gemini), I tried making my own personal Chrome extension. Over this four-part series, I will write about "systematized thinking"—the process of utilizing AI to build tools and independently maintaining them. In Part 1, I'll share the developmental dialogue process: "How did I instruct the AI, what information did I provide, and how did we complete the prototype?" 1. A Prompt That Says: "Don't Guess, Ask for the Information You Need" As the very first step in development, I threw this prompt directly at Gemini itself. "I want to save Gemini's responses to a spreadsheet using a Chrome extension. Tell me how to build it without using your imagination. If you need any specific information, please point it out." The key here lies in two constraints: "without using your imagination" and "point out if you need information." When you try to build a web data extraction tool using AI, the AI often tends to "guess" the internal structure of the webpage (like HTML tags and class names) on its own and write the code. And even when you test this supposedly completed code, you fall into the trap of it not working because it doesn't align with the actual screen structure. To avoid this trap, I explicitly communicated, "Don't guess on your own. If there's missing information, I want you to demand it from the human side." 2. A Game of Catch with AI Using DevTools When I threw this prompt, the AI returned the following response: AI: "Understood. To create code that works reliably while eliminating guesswork, please retr
AI 资讯
I Built The Most Advanced Job Application Tracker
If you're actively applying for jobs, you probably know the struggle: Did I already apply to this company? Which resume version did I send? What salary did I mention when I applied? What was the budget mentioned in the job posting? When did I apply for this one? Which interviews are scheduled this week? What were the HR contact details again? What exactly were the requirements for this role? When is my next interview? Where did I even find this posting? How many of my applications are actually turning into interviews? Every one of those is answerable. The problem is that the answers are scattered across a spreadsheet, a notes app, your inbox, and your memory — and reassembling them takes longer than the follow-up you were trying to send. Spreadsheets are where most people start, and they hold up until somewhere around application number twelve. After that, searching, filtering, and keeping the thing current becomes its own small job — and a spreadsheet still won't tell you that six applications have been sitting in "Applied" for a month, or whether your last twenty went better than the twenty before them. That's why I built HireLoop — an advanced job application tracker meant to reduce the mental load of a job search rather than add to it. Live app: hireloop.yogeshchavan.dev — free to use, with a demo account if you'd rather look around before signing in. Check out the application demo video below: Check out some preview images of the application The short version With HireLoop you can: Track every application in one place — status, dates, salary, source, and links See where your search stands at a glance on a dashboard Move applications through a Kanban pipeline Search, filter, and sort as the list grows See interviews and deadlines on a calendar Analyse interview rates, offer rates, application trends, and which sources actually work Store notes, resume versions, HR contacts, salary details, and job links per application Mark the ones that matter as favourites Kee
AI 资讯
A Practical Guide to Converting Inches, Centimeters, Meters, Feet and Millimeters
If you work with measurements often enough, you eventually run into the same problem: the value you have isn't in the unit you need. A product specification might be in inches. A construction drawing might use feet. A European supplier might give you dimensions in centimeters or millimeters. The actual formulas are usually simple. Finding the right conversion, avoiding rounding mistakes, and checking a large list of values can be more annoying than the math itself. Here are the conversions I use most often and a few practical ways to work with them. Inches to centimeters The basic relationship is: 1 inch = 2.54 centimeters So the formula is: centimeters = inches × 2.54 For example: 10 inches × 2.54 = 25.4 cm This is probably the most common conversion when moving between imperial and metric measurements. If you just need to check a value quickly, Pulgadas a CM has an interactive converter along with a conversion table and frequently asked questions. Centimeters to inches Going in the opposite direction means dividing by 2.54: inches = centimeters ÷ 2.54 For example: 25.4 cm ÷ 2.54 = 10 inches You can use the CM a Pulgadas converter when you need to work in this direction. This is particularly useful when a measurement is provided in centimeters but the product, tool, or specification you're working with uses inches. Meters to inches Meters are larger units, so the conversion factor is correspondingly larger. One meter contains approximately: 39.3700787 inches Therefore: inches = meters × 39.3700787 For example: 2 meters ≈ 78.7401574 inches For a quick calculation, you can use the Metros a Pulgadas converter . This conversion can come up when working with room dimensions, furniture measurements, fabric, sports equipment, or other products where metric and imperial specifications are mixed. Inches to meters The reverse calculation is: meters = inches × 0.0254 For example: 100 inches × 0.0254 = 2.54 meters The Pulgadas a Metros converter is useful when an imperial meas
AI 资讯
Spring Boot For Beginner
🚀 Building a REST API with Java Spring Boot: A Practical Beginner’s Guide If you're coming from Java and want to move into backend development, Spring Boot is one of the best frameworks to learn. It removes a lot of the boilerplate traditionally associated with Spring and makes it surprisingly easy to build production-ready REST APIs. In this article, we'll build a simple Blog REST API using: ☕ Java 🌱 Spring Boot 🌐 Spring Web 🗄️ Spring Data JPA 🐘 PostgreSQL 📦 Maven 🧪 Postman By the end, we'll have an API that can: Create a blog post Get all blog posts Get a post by ID Update a post Delete a post 1. What is Spring Boot? Spring Boot is a framework built on top of the Spring Framework that makes it easier to create Java applications. Without Spring Boot, you often need to configure many things manually. Spring Boot gives us: Auto-configuration Embedded servers Starter dependencies Production-ready features Easy REST API development A simple Spring Boot application can be started with: @SpringBootApplication public class BlogApplication { public static void main ( String [] args ) { SpringApplication . run ( BlogApplication . class , args ); } } That's enough to start our application. 2. Create the Spring Boot Project The easiest way to create a Spring Boot project is through Spring Initializr . Choose: Project: Maven Language: Java Spring Boot: Latest stable version Packaging: Jar Java: 17+ Add these dependencies: Spring Web Spring Data JPA PostgreSQL Driver Validation Lombok Your project structure will look something like: src └── main └── java └── com.example.blog ├── BlogApplication.java ├── controller ├── service ├── repository ├── entity └── dto This separation will become important as our application grows. 3. Create the Blog Entity Let's create a simple BlogPost entity. @Entity @Table ( name = "blog_posts" ) public class BlogPost { @Id @GeneratedValue ( strategy = GenerationType . IDENTITY ) private Long id ; @NotBlank private String title ; @NotBlank @Column (
AI 资讯
Java Spring Boot Logging: Log Levels, Logback, JSON Logs & Production Best Practices
Production-Grade Logging in Spring Boot: A Complete Guide to Logging Levels, Files, JSON Logs, Correlation IDs, and Best Practices Logging is one of the most important parts of a production backend system. When everything works, you may not think much about logs. But when production starts returning 500 errors at 2 AM, a customer reports that an API is failing, or a payment request behaves unexpectedly, logs become one of your most important debugging tools. Poor logging makes production debugging painful. Good logging helps you answer: What happened? When did it happen? Which user triggered it? Which request caused it? Which service handled it? How long did it take? What failed? Why did it fail? What should we investigate next? In this article, we will build a production-grade logging strategy for a Java Spring Boot application . 1. What Does Production-Grade Logging Mean? Production-grade logging is not simply: log . info ( "User created" ); Production logging should be: Structured Searchable Consistent Secure Configurable Environment-aware Correlated across requests Useful during debugging Suitable for monitoring and alerting A good logging architecture might look like this: Spring Boot Application | v Logback | +---- application.log | +---- error.log | +---- audit.log | +---- access.log | v Log Aggregation | +---- ELK +---- Grafana Loki +---- CloudWatch +---- Datadog +---- Splunk The goal is not to log everything. The goal is to log the right information at the right level . 2. Understanding Log Levels Spring Boot uses SLF4J as the logging abstraction and commonly uses Logback as the underlying logging implementation. The most common log levels are: TRACE DEBUG INFO WARN ERROR The order represents increasing severity. TRACE TRACE is the most detailed logging level. Example: log . trace ( "Entering calculateInvoice() with customerId={}" , customerId ); Use TRACE for very detailed diagnostic information. Usually: Production: OFF Development: Sometimes ON Debugging
AI 资讯
Building a Leak-Safe gRPC Frame Decoder on Reactor Netty
This is the second article in my grpc-reactor series. The first article explains why I chose to build the runtime directly on Reactor Netty and where its compatibility boundary sits. This article moves one layer down into the Stage 1 protocol implementation: the frame decoder that every RPC shape relies on. gRPC protobuf messages are not written directly as raw bytes into HTTP/2 DATA frames. Every message starts with a five-byte envelope: byte 0 bit 0 indicates compression; bits 1-7 must be zero bytes 1-4 unsigned big-endian payload length byte 5..n protobuf message, or its compressed representation Encoding this envelope is straightforward. The difficult part is decoding it without assuming that one input buffer contains one complete frame. HTTP/2, TCP, and Reactor Netty do not promise that buffer boundaries will line up with gRPC message boundaries. This post describes the Stage 1 protocol layer. The project has since progressed beyond it, but the ownership and bounded-decoding rules introduced here remain the foundation for the later transport stages. Encoding Must Define Ownership The contract of GrpcFrameCodec.encode is deliberately explicit: the returned frame and the input message have independent lifetimes. Encoding must not move the input reader index or release the input buffer. The implementation currently copies the readable bytes into a byte array before applying compression: public static ByteBuf encode ( ByteBufAllocator allocator , ByteBuf message , GrpcCompression . Codec compression ) { boolean compressed = ! compression . name (). equals ( "identity" ); byte [] payload = new byte [ message . readableBytes ()]; message . getBytes ( message . readerIndex (), payload ); if ( compressed ) { payload = compression . compress ( payload ); } return allocator . buffer ( GrpcFrameCodec . HEADER_SIZE + payload . length ) . writeByte ( compressed ? 1 : 0 ) . writeInt ( payload . length ) . writeBytes ( payload ); } This is not a zero-copy implementation, and
AI 资讯
I Tried Building JavaScript Games Without a Game Engine. Here's What I Learned
I am a digital marketer, not a professional developer or game developer. Most of my career has been focused on SEO, growth marketing, paid acquisition, content, and digital strategy. When I started building GamesMom, however, I found myself learning much more about web development than I expected. GamesMom is a collection of free educational games and learning activities for kids that run directly in the browser. The site includes math games, word games, typing games, memory games, puzzle games, classroom games, quizzes, and other interactive activities. The idea was simple: make games that children can open and play without downloading an application or creating an account. I initially assumed that building browser games would require a dedicated game engine or a large JavaScript framework. After experimenting with different approaches, I found that many of the games I wanted to create could be built with ordinary HTML, CSS, and JavaScript. That was probably the most useful lesson I learned from the project. You don't always need a complicated technology stack to create an interactive web experience. I Started With the Simplest Approach When you're not a professional developer, it is tempting to look for the most sophisticated solution available. I did this too. I spent time looking at frameworks, game engines, libraries, and different ways of structuring interactive applications. Eventually I started asking a much simpler question: what does this particular game actually need? A basic educational game might need to display a question, accept an answer, update a score, show feedback, and move to the next question. Another might need a timer, a few buttons, and some randomization. Those requirements don't automatically justify a game engine. For simple browser games, the browser already provides a lot of what you need. HTML, CSS and JavaScript Can Go a Long Way The basic combination is surprisingly capable. HTML provides the structure of the page. CSS controls the v
开发者
DBNavigator – An DataGrip-inspired Database IDE Built with JavaFX
After months of development, I'm excited to share DBNavigator, a cross-platform database IDE that I've been building from scratch using Java and JavaFX. ✨ Current Features ✅ PostgreSQL support ✅ MySQL support ✅ Modern Datagrip-inspired UI ✅ Multi-tab SQL editor ✅ Syntax highlighting ✅ Schema explorer ✅ Query execution ✅ Professional dark theme ✅ Cross-platform (Windows, Linux & macOS) This project has been an incredible learning journey in desktop application development, JavaFX UI design, database connectivity, and IDE architecture. I'm sharing it with the developer community because I'd genuinely appreciate your honest feedback. I'd love to hear your thoughts on: UI/UX design Performance Missing features Overall developer experience Architecture and code quality Any bugs or improvements you notice Whether you're a Java developer, DBA, or someone who works with databases every day, your feedback would mean a lot and help shape the next version of the project. ⭐ If you find the project interesting, please consider giving it a star on GitHub. GitHub: DBNavigator Thank you for taking the time to review it. Every suggestion, issue report, and critique is greatly appreciated! 🙌
AI 资讯
The SVG Path Data Format, Explained (M, L, C, Q, A, Z)
If you've opened a <path d="..."> string and had no idea what you were looking at, here's the short version: it's a tiny drawing language. A pen moves around a coordinate space, and each letter in the string is an instruction telling it what to do next. TL;DR M / m moves the pen, L / l draws a straight line, C / c and Q / q draw bezier curves, A / a draws an arc, Z / z closes the shape. Uppercase is absolute coordinates, lowercase is relative to the pen's current position. A visual path editor drags the exact same numbers you'd type by hand, it just shows you the curve instead of making you compute it. Reading a path string <path d="M10 10 L90 10 L90 90 Z" /> Broken down: move to (10, 10), draw a line to (90, 10), draw a line to (90, 90), close the path back to the start. That's a right triangle. Every path, no matter how complex, is this same pattern: a command letter followed by however many numbers that command needs, repeated. The command set Command Name What it takes M / m Move to x, y L / l Line to x, y C / c Cubic bezier control1 x/y, control2 x/y, end x/y Q / q Quadratic bezier control x/y, end x/y A / a Arc rx, ry, rotation, large-arc-flag, sweep-flag, end x/y Z / z Close path none C and Q are both bezier curves, the difference is one control point ( Q ) vs two ( C ). Two control points give you more independent influence over each end of the curve; one control point gives you a simpler, more symmetric curve. There are also shorthand continuations ( S / s , T / t ) for chaining smooth curves without repeating a control point, but the six above are what you'll hit constantly. A is the one people avoid writing by hand. Six parameters, two of which are flags (0 or 1) that determine which of four possible arcs you get for the same radii and endpoints. Flip one and you're not slightly off, you're on the opposite side of the ellipse. Absolute vs relative is the part that bites Every command above has an uppercase and lowercase form, and it's not cosmetic: <!-- a
AI 资讯
I Built a Self-Hosted AI Support Widget with Spring Boot (No Monthly SaaS Fees)
Every new SaaS seems to embed ChatGPT these days. Most AI support solutions rely on third-party platforms, monthly subscriptions, and vendor lock-in. While they're great products, I wanted something different. I wanted complete ownership. I wanted to deploy everything on my own server, use my own OpenAI API key, customize every part of the experience, and embed the widget into any website with a single script tag. So I built my own self-hosted AI support widget using Spring Boot and Vanilla JavaScript. Why I Built It When building small products and websites, I realized that customer support quickly becomes a problem. Users have questions about pricing, features, returns, or simply get stuck. Most developers solve this by integrating services like Intercom, Crisp, or Tidio. Those platforms are excellent, but they also mean: Monthly subscription costs Vendor lock-in Customer conversations stored on third-party platforms Limited customization Another external dependency I wanted something that developers could completely own. The Goal The goal was simple. Build an AI-powered customer support widget that developers can deploy on their own server and integrate into any website in less than a minute. The widget should: Answer customer questions using AI Learn from a custom knowledge base Match the company's branding Store conversation history Allow human handoff Be easy to deploy Require only one script tag to embed Technology Stack Java 17 Spring Boot 3 Spring Security Spring Data JPA Thymeleaf Vanilla JavaScript H2 Database (MySQL supported) OpenAI API Architecture The overall architecture is intentionally simple. Visitor │ ▼ AI Chat Widget (Vanilla JavaScript) │ ▼ Spring Boot REST API │ ▼ OpenAI API │ ▼ Database (H2 / MySQL) Keeping the frontend framework-free makes the widget lightweight and easy to embed into virtually any website. One-Line Integration Adding the widget to a website only requires a single script. <script src="/widget/widget.js" data-api-base=""></sc
AI 资讯
How to Repair Corrupted PDFs in the Browser with Vue 3 and pdf-lib
A corrupted PDF is one of the most frustrating file problems. You have important content inside, but the document won't open, opens with garbled text, or shows missing pages. The file might be damaged from a bad download, a converter error, or a storage glitch. Recovering content from a broken PDF doesn't require complex forensic tools. Often, the individual pages are still readable — it's the document's structure (cross-reference tables, object streams) that's damaged. By extracting pages one by one into a fresh PDF, we can bypass the structural corruption. Here's how to build a browser-based PDF repair tool with Vue 3 and pdf-lib . The repair strategy The core insight: PDF structure and page content are somewhat independent . A PDF can have a broken cross-reference table or missing trailer objects, but the actual page content streams may still be perfectly readable. The repair approach: Load the damaged PDF and attempt to read each page For each successfully read page, copy it to a new PDF document Discard unreadable pages (they're lost anyway) Save the new document This is fundamentally different from "fixing" the original PDF. We're extracting what we can and rebuilding from the ground up. The stack Vue 3 with Composition API pdf-lib for PDF reading and page extraction Vite for bundling The core implementation < script setup lang= "ts" > import { ref } from ' vue ' import { PDFDocument } from ' pdf-lib ' const file = ref < File | null > ( null ) const totalPages = ref ( 0 ) const recoveredPages = ref ( 0 ) const repairing = ref ( false ) const result = ref < Uint8Array | null > ( null ) const error = ref < string | null > ( null ) async function repairPdf () { if ( ! file . value ) return repairing . value = true error . value = null try { const arrayBuffer = await file . value . arrayBuffer () const damaged = await PDFDocument . load ( arrayBuffer , { ignoreEncryption : true , updateMetadata : false , }) totalPages . value = damaged . getPageCount () const resu