Pixel

Pixel Updates & Events

Server Updates & Events

Announcements

DEV Community

Automating Construction Invoice Workflows with n8n, Gmail and Notion

automation #javascript #productivity #showdev Managing construction operations involves more than tracking projects. Businesses also need to manage supplier invoices, project costs, equipment hire and payment statuses. I explored these challenges through Construction Operations Automation, a portfolio prototype using Notion, n8n, Gmail and JavaScript. The Problem When invoices arrive through email, manually entering them into a project database creates several risks: Duplicate invoices Missing supplier information Incorrect payment statuses Inconsistent monthly reporting Poor visibility of project expenditure My objective was to design a workflow that reduces repetitive administration. The Solution The prototype connects email processing with structured Notion databases. The workflow follows this sequence: Gmail receives an invoice. n8n retrieves the message. JavaScript extracts invoice information. Required fields are validated. The workflow checks for an existing invoice. A project cost record is created or updated. The supplier-month summary is created or updated. The design also considers changes such as an invoice moving from Unpaid to Paid. Example Invoice A fictional test invoice contains: Invoice reference: TEST-001 Supplier: Demo Building Supplies Project: Sample Residence Amount: 350 Status: Unpaid The invoice reference helps the workflow identify existing records and avoid unnecessary duplicate creation. What I Tested The repository documents tests involving new invoices, duplicate handling, payment-status updates, missing fields and supplier-month summary creation. Important Limitation This is a sanitized portfolio reconstruction, not a production deployment. One important improvement is still needed: monthly supplier totals should be recalculated from all relevant project cost records. Without that, multiple invoice updates could produce incorrect aggregate values. Other future improvements include error retries, audit logs, attachment handling and production monitoring. What I Learned This project helped me explore the relationship between business processes, database design and workflow automation. A working automation is not just about connecting applications. It also needs reliable validation, consistent records and careful handling of repeated events. GitHub: https://github.com/tasnem-tech/construction-operations-automation Project website: https://tasnem-tech.github.io/construction-operations-automation/ I'm interested in how other developers approach reliable invoice processing and no-code/low-code automation.


Powered by Readybot.io

**DEV Community** ## Automating Construction Invoice Workflows with n8n, Gmail …

DEV Community

A stranger tested my SaaS and found my Free plan was hiding the product

I run Formkove, a form backend. You point your HTML form at an endpoint and submissions go to your inbox, a webhook, Slack and so on. Paid plans add AI that tags each submission, reads the sentiment and routes it to the right place. A few days ago someone found Formkove in a Reddit thread, signed up, hosted a real form, sent submissions through it and emailed me four things they'd change. I hadn't asked. Near the end they wrote "the backend itself worked really well for me, so this is mostly about letting people see that sooner." They were right. Nothing big was broken, people just couldn't see the part I charge for. I shipped all four changes the same day. 1. Free users never saw the AI My homepage sells sorting by sentiment and your own tags. But on Free the AI switch was disabled, so someone deciding on $12 a month had never seen one tagged submission. They suggested AI on the first ~25 submissions. I went with a real trial: every Free account gets Pro's AI (tags, sentiment, routing and up to 3 destinations like Slack or webhooks) for the first 50 submissions or 30 days, whichever comes first. No card, nothing to turn on. Most of the work was deciding what counts: The 30 days start at your first real submission, not at signup. People often take a week to wire up the form, and a signup clock would burn half the trial before they see anything. Spam and test submissions don't use it up. Otherwise bots could eat the 50 before a real lead came in. New forms get starter tags (lead, support, feedback). The AI tags against your own list, so without these the first submission would have no tags. Tags stay after the trial ends. New submissions go back to the Free spam check. Existing Free users got it too. Anyone who has ever paid can't get one, so cancelling doesn't buy a fresh trial. The sidebar shows how much is left, and each notification email has one line with the count. The upgrade nudge only shows up near the end. 2. There was no way to send a test The empty Submissions page had one button, "Open setup", and Setup had the endpoint and snippets but no way to try it. They had to host a form somewhere else to see anything. With a trial that's worse, since the first thing you want is to watch the AI work. Now there's a "Send test submission" button on both pages. It sends a sample lead through the real pipeline (spam check, AI, email, webhooks) and the row is marked as a test everywhere. They also suggested a curl example. I skipped it because we block curl at the edge, a lot of the bot traffic on form endpoints comes from it. 3. The AI said "Neutral" when nobody asked it This was a real bug. With AI off, every submission still showed a "Neutral" badge, including "Could you quote a redesign... Budget about 4000 USD" and casino spam. As they put it, that makes the sentiment model look weak to someone deciding on Pro. The model never looked at sentiment. Free gets a cheap spam-only check, but the parser returned the same shape as a full analysis, and that shape needed a sentiment: return {
spam_score: Math.round(spamScore * 1000) / 1000,
is_spam: spamScore >= SPAM_THRESHOLD,
assigned_tags: [],
sentiment: "neutral",
}
"neutral" was a placeholder to keep the type happy, and the database column had the same default. The fix was sentiment: null, a migration so the column allows it, and a locked "Sentiment and tags · Pro" chip where the badge was. A fake label on casino spam is the last thing you want on the day you launch a trial that's meant to show off the AI. 4. The notification email led nowhere The "New submission" email only linked to the homepage. Now every notification has a "View submission" button that opens that exact submission, which during the trial is where you see the tags. They also suggested a line like "Pro would have tagged this: lead · positive". I skipped it. I'd just removed one made-up label and the trial shows real tags anyway. What I took from it Error tracking can't flag a value that's wrong but ...

Powered by Readybot.io

**DEV Community** ## A stranger tested my SaaS and found …

DEV Community

One Brief, 63,000 Words, 42 Illustrations, and a Working Flipbook: A Qwen4 Case Study

One brief. 40 chapters. 42 illustrations. A working browser-based flipbook. What happens when you give a newly released flagship model a project that can't be completed in one shot? When a new flagship model appears, the usual first tests are familiar: ask it a hard reasoning question, throw a coding benchmark at it, or have it build a small app. I wanted to try something less tidy. I gave Qwen 4 Preview a project with several kinds of work tangled together: write a complete children's mystery, create a consistent illustration system for it, and turn the result into an interactive book that actually works in a browser. The output was: A 63,258-word novel across 40 chapters. 42 SVG illustrations built around a reusable library of nine characters. A single-file, 652-page HTML flipbook. A headless browser run that reported zero runtime errors and zero text overflow. The run took about 1 hour and 42 minutes and involved 242 assistant turns. I ran the model through Jouzu, a coding-agent harness. The source repository contains the prompts and generated artefacts. You can open the finished flipbook here. This isn't a benchmark, and I'm not claiming that one run settles anything about the model. It is a practical look at how Qwen 4 Preview handled a long-running task where the requirements had to survive across multiple stages. Why this particular test? The project is called Pip Parker and the Bureau of Lost Things. Pip is a boy who labels everything. When his brother's stuffed dinosaur goes missing, the trail leads to a bureaucratic animal office that files the town's lost things. There's a mystery to solve, a set of clues to pay off, and a midpoint reframe: the apparent thief isn't stealing things; he's saving them. A children's mystery makes a useful stress test because continuity failures are easy to spot. A character can't suddenly know something they haven't learned. An object can't be in two places at once. A clue introduced early has to matter later. And the ending has to resolve what the opening set in motion. Then I added two more layers. The book needed illustrations, but not 42 unrelated images. It needed a consistent visual language: nine reusable characters, an eight-colour palette, and SVG artwork that reused the same character definitions. Finally, it needed to become a real reading experience. Not a PDF export or a long HTML page, but a flipbook with page turns, navigation, bookmarks and controls. That combination made the task interesting. The agent had to move between narrative generation, structured visual assets and application development without treating them as three unrelated jobs. The brief: constraints first I deliberately made the brief specific. I didn't want the model to decide halfway through that 20 chapters were enough, change the cast, or quietly simplify the illustration requirements. The novel specification called for: 40 chapters, each between 1,400 and 1,700 words. A target of roughly 60,000 words. A fixed cast and timeline. Five explicit plant-and-payoff pairs. A continuity ledger tracking day-by-day events and object ownership. A fixed final sentence. Kid-safe content rules: no death or gore, and a lonely rather than evil antagonist. The plant-and-payoff requirements were especially useful. A detail introduced in one chapter had to become meaningful several chapters later. The model couldn't just make each chapter locally plausible; it had to preserve information for future use. The illustration brief was equally constrained: A nine-character SVG library. A locked eight-colour palette. One cover illustration and one illustration for each of the 40 chapters. The same SVG definitions reused across the plates. The point of specifying these constraints wasn't to make the prompt impressive. It was to make success and failure observable. It took one magic prompt The prompts are preserved verbatim in PROMPT.md. 1. Write the book and create the art system. The initial brief produced the novel, the i...

Powered by Readybot.io

**DEV Community** ## One Brief, 63,000 Words, 42 Illustrations, and …

DEV Community

Analysing 900 IT Help Desk Tickets with Python and SQL

Tags: #python #sql #datascience #beginners How can an IT support team identify recurring problems, measure service quality and improve resolution times? I explored these questions through my Help Desk Ticket Analysis project using Python, Pandas, SQL and Matplotlib. The Dataset The project uses 900 synthetic IT support tickets from 2025. The dataset contains information about issue categories, priorities, support teams, response times, resolution times and service-level agreement (SLA) performance. Because the data is synthetic, the findings demonstrate analytical techniques rather than the performance of a real organisation. Data Cleaning Before calculating performance metrics, I worked with common data-quality problems, including: Duplicate ticket identifiers Missing values Inconsistent support-team names Incorrect timestamp formats Inconsistent issue categories One important decision was not to automatically discard every incomplete ticket. A record missing an analyst name may still contain useful resolution and SLA information. Key Results The repository reports the following results for its synthetic dataset: Metric Result Total tickets 900 Average first response 0.91 hours Average resolution time 7.74 hours SLA compliance 70.3% Average satisfaction 4.03/5 The most common issue category was Application Error. Why SLA Compliance Matters A low average resolution time can look positive while hiding serious delays for high-priority tickets. That is why I compared multiple indicators rather than relying on one overall average. The analysis considers ticket volume, issue categories, resolution time, SLA compliance and customer satisfaction. Tools Used Python and Pandas for cleaning and aggregating records. SQL for operational reporting queries. Matplotlib for visualising ticket patterns. Jupyter Notebook for exploratory analysis. What I Would Improve Future extensions could include an interactive Power BI dashboard, ticket backlog analysis, reopen rates, first-contact resolution and automated ticket categorisation. Conclusion This project demonstrates how data analysis can support IT service management. The main lesson was that operational performance should be evaluated through several connected metrics. GitHub: https://github.com/tasnem-tech/help-desk-ticket-analysis Which service desk KPI do you find most useful: response time, resolution time or SLA compliance?

Powered by Readybot.io

**DEV Community** ## Analysing 900 IT Help Desk Tickets with …

DEV Community

Analysing UK Retail Sales and Profitability Using Python and SQL

Tags: #dataanalytics #python #sql #datascience Revenue tells us how much a business sells. Profit tells us how much value it keeps. Understanding the difference is an important part of business data analysis. I explored this through my UK Retail Sales Performance Analysis project. Project Overview The project analyses 2,500 synthetic UK retail transactions from 2025. The dataset covers five regions, four product categories and two sales channels. My objective was to practise transforming transaction records into meaningful business metrics. Analytical Workflow The workflow includes: Importing raw transaction data. Checking missing values and duplicates. Standardising inconsistent fields. Calculating revenue and profitability metrics. Running SQL queries. Creating visualisations. Interpreting business results. The raw dataset intentionally contains minor data-quality issues to demonstrate cleaning techniques. Key Results The repository reports: KPI Result Total revenue £320,707 Total profit £116,331 Profit margin 36.3% Return rate 9.2% Top revenue region Midlands Top profit category Electronics These are results from simulated transactions, not real UK retail market statistics. Example SQL Query One of the included queries calculates regional revenue: SELECT
region,
ROUND(SUM(revenue_gbp), 2) AS revenue
FROM retail_sales
GROUP BY region
ORDER BY revenue DESC;
This produces a ranked view of revenue contributions by region. Turning Analysis into Decisions The analysis raises several business questions. Should management investigate why the Midlands performs strongly? Are high-revenue products also generating strong profits? Do discounts improve sales volume at the expense of profitability? Could high return rates reduce the value of certain product categories? The synthetic dataset cannot establish real-world answers, but it provides a useful environment for practising analytical reasoning. Future Improvements I would like to extend this project with Power BI, customer segmentation, forecasting and automated reporting. Conclusion This project helped me connect Python and SQL analysis with business decision-making. The most useful takeaway was that revenue, profit, returns and discounts need to be examined together. GitHub: https://github.com/tasnem-tech/uk-retail-sales-analysis What business metric would you investigate first when reviewing a retail dataset?

Powered by Readybot.io

**DEV Community** ## Analysing UK Retail Sales and Profitability Using …

DEV Community

I Built a Self-Hosted 4-in-1 E-Commerce Engine in Laravel (Retail, Wholesale, B2B & Subscriptions)

Hey Laravel devs 👋 I’m Stephan—an independent PHP/Laravel developer, and this is my first time posting in the Laravel community here on DEV. After years of building custom WordPress plugins and client Laravel apps, I wanted to start sharing the architecture behind what I'm building and connect with other Laravel builders. The Problem I Wanted to Solve Whenever a client or solo founder wants to launch an online store, they usually hit a wall: either pay escalating monthly SaaS fees and transaction cuts, or wrestle with bloated legacy plugins just to handle basic commerce rules outside of standard retail. I wanted a lightweight, self-hosted Laravel foundation that could handle four distinct commerce models out of the box without duct-taping a dozen third-party packages together: Retail: Standard direct-to-consumer storefront and cart Wholesale: Tiered bulk pricing and minimum order rules B2B: Account-based purchasing workflows Subscription Boxes: Recurring billing cycles Under the Hood of ShopKeeprr v1.1 I just shipped ShopKeeprr v1.1 as a free, self-hosted core engine. A few of the Laravel implementation details I enjoyed building most: WordPress-Style In-App Marketplace: Instead of forcing store owners to drop into SSH and run Composer commands to add features, I built an in-app extension installer using native HTML overlays, package archive extraction, and SHA checksum verification. Zero-CLI Web Installer (install.php): Many self-hosted users still deploy on cPanel/shared hosting where terminal access is limited or intimidating. ShopKeeprr includes a browser-based setup wizard that validates PHP extensions, writes the .env file, generates APP_KEY, and runs migrations automatically. Unified Payment Webhooks: Built-in Stripe and PayPal webhook handlers engineered to stay consistent across one-time retail carts, B2B invoices, and recurring subscriptions. Check Out the Code or Demo The core engine is completely free to download and self-host: GitHub: https://github.com/DevByStephan/ShopKeeprr Live Demo: https://demo.shopkeeprr.com Since I'm actively building out the extension ecosystem, I'd love feedback from fellow Laravel developers—how do you usually structure modular plugin/extension architectures in your Laravel apps?

Powered by Readybot.io

**DEV Community** ## I Built a Self-Hosted 4-in-1 E-Commerce Engine …

DEV Community

Analysing 68 Years of Atmospheric CO Data with Python

Tags: #python #datascience #climatechange #dataanalytics One of the most interesting things about data analysis is the opportunity to explore real scientific questions. For my Keeling Curve Climate Data Analysis project, I worked with historical atmospheric carbon dioxide measurements and global temperature data. What Is the Keeling Curve? The Keeling Curve records the long-term increase in atmospheric carbon dioxide concentrations, using measurements that began at Mauna Loa in 1958. The project explores data from March 1958 through May 2026, alongside global temperature anomaly data. Project Objectives I wanted to investigate: How atmospheric CO₂ concentrations changed over time Whether the average growth rate increased How seasonal patterns appear in the data How CO₂ and global temperature trends compare How to communicate scientific findings through visualisations Tools and Data The project uses Python, Pandas, NumPy, Matplotlib and Jupyter Notebook. Its documented data sources include NOAA atmospheric CO₂ measurements and NASA GISS temperature anomaly data. Key Findings The repository reports: Measure Result CO₂, March 1958 315.7 ppm CO₂, May 2026 432.3 ppm Total increase 116.6 ppm Percentage increase 36.9% 1960s annual growth 0.86 ppm/year 2020s annual growth 2.67 ppm/year The analysis also reports a correlation of r = 0.966 between overlapping CO₂ and temperature time series. This correlation should be interpreted carefully. Two strongly trending series can produce a high correlation, and correlation alone does not establish causation. A Data-Quality Challenge One interesting challenge involved the imported CO₂ dataset. The expected column structure did not match the retrieved data fields. Instead of immediately analysing the imported table, the project involved inspecting the structure, correcting column mappings and validating the results. This reinforced an important lesson: Never assume that imported data is analysis-ready. Limitations Mauna Loa is a monitoring location, not a direct measurement of every location on Earth. The comparison also uses different observational datasets with their own measurement and modelling assumptions. Further analysis could examine detrended relationships, uncertainty and seasonal adjustment. Conclusion This project helped me practise time-series analysis, data validation and scientific visualisation using real environmental data. GitHub: https://github.com/tasnem-tech/keeling-curve-climate-data-analysis What scientific dataset would you recommend for a future Python analysis project?

Powered by Readybot.io

**DEV Community** ## Analysing 68 Years of Atmospheric CO Data …

DEV Community

Node.js Message Processing Rate-Limiting: Choosing a Queue Service

Short answer: To implement rate-limiting in Node.js message processing, use a queue service for delayed work and keep a durable per-destination token bucket beside the delivery record. The queue moves the message. The limiter decides when processing may start. Choice Good fit Recovery trade-off Queue plus per-destination bucket Many recipients with separate quotas More state to inspect One global concurrency limit One downstream quota One failing recipient affects others Scheduled database polling Low volume and simple recovery Claim and polling logic become yours I would use the first row when the operational question is “which destination is aging?” That answer is more useful than a single queue-depth number. The cost is clear state and an atomic limiter. How should a Node.js queue service implement rate-limiting for message processing? The queue service owns visibility, delayed re-enqueueing, and worker movement. The application owns the delivery record, recipient policy, and idempotency decision. A timeout is an unknown result, not proof that the receiver did nothing. For one marketplace event sent to two destinations, keep separate delivery records. The event ID identifies the business event; the delivery ID identifies one destination-specific job. Reusing one status for both would make an outage at one destination block recovery. Keep it boring. type WebhookDelivery = {
deliveryId: string;
eventId: string;
destinationId: string;
state: 'pending' | 'in_flight' | 'succeeded' | 'retryable' | 'dead';
attempt: number;
nextAttemptAt: string;
idempotencyKey: string;
};
I first treated a delayed queue as the whole solution. That was the wrong boundary. Delay says when a job can be visible again; it does not say whether the destination has capacity then. Those are separate clocks, and collapsing them makes a rate-limited message processor hard to recover. Suppose one worker group allows 20 attempts per second across all recipients. A failing destination can consume retry slots while a healthy destination receives new order events. A per-destination bucket isolates that failure. For example, a policy might allow 5 attempts per second with a burst of 10. Those are inputs from the recipient contract and observed behavior, not universal defaults. The worker checks immediately before the network call. If there is no token, it reschedules without increasing the attempt count. The limiter must be atomic. Two workers reading the last token and both spending it is a distributed race, not an unlikely corner case. type Limit = {
tokens: number;
capacity: number;
refillPerSecond: number;
checkedAtMs: number;
};

function takeToken(limit: Limit, nowMs: number): Limit | null {
const elapsed = Math.max(0, nowMs - limit.checkedAtMs) / 1000;
const tokens = Math.min(
limit.capacity,
limit.tokens + elapsed * limit.refillPerSecond,
);

if (tokens < 1) return null;
return { ...limit, tokens: tokens - 1, checkedAtMs: nowMs };
}
The in-memory function describes policy only. Production storage needs a transaction or another atomic read-modify-write operation. How do retries avoid duplicate deliveries? Classify outcomes as accepted, temporary failure, permanent failure, or unknown result. A temporary failure gets a later attempt. A permanent failure stops automatic delivery. An unknown result may be retried, but it must reuse the same idempotency key because the receiver may already have accepted the event. Test the awkward case directly: the sender times out after the receiver accepts the event. Run 3 attempts in the test, then verify that the receiver applies the order update once. Add 20 workers competing for a bucket with capacity 10 and assert that the atomic limiter never grants more than the configured allowance. Make the test use fake time, because wall-clock assertions are brittle and do not explain recovery. Observe age of the oldest pending delivery, retry rate by destination, unknown-result count,...

Powered by Readybot.io

**DEV Community** ## Node.js Message Processing Rate-Limiting: Choosing a Queue …

DEV Community

Patch Tuesday November 2026: One Zero-Day, 64 Critical Fixes Among 1,097 CVEs

tags: security, cybersecurity, windows, patchtuesday Microsoft's November 2026 release fixes 1,097 vulnerabilities, 64 of them rated Critical. Of those, one is already exploited in the wild. Patch Microsoft Outlook first: CVE-2026-124194 is being exploited in the wild and can expose authentication information when a user opens a crafted message file. Prioritize Mariner updates alongside it; both vulnerabilities are in CISA KEV and require customer patching. Patch these first CVE-2026-124194 (Moderate, CVSS 6.5) — Microsoft Office Outlook: Spoofing — exploited in the wild, CISA KEV CVE-2026-123158 (Important, CVSS 8.1) — Mariner: see advisory — CISA KEV CVE-2026-124194 lets an unauthenticated attacker send a specially crafted message file that, when opened in an affected Outlook version, can trigger a connection to an attacker-controlled network location without the usual external-content warning. That connection could disclose authentication information usable to impersonate the user. Exploitation requires user interaction and network connectivity. Deploy the Outlook patch now; exploitation is active and the vulnerability is in CISA KEV. CVE-2026-123158 is also in CISA KEV. Patch affected Mariner systems as a priority; remediation requires a customer-applied update. Also notable CVE-2026-118207 (Critical, CVSS 8.8) — Microsoft Exchange Server: Remote Code Execution CVE-2026-118146 (Critical, CVSS 8.8) — Active Directory Domain Services: Remote Code Execution CVE-2026-121663 (Important, CVSS 8.8) — Visual Studio Code: Security Feature Bypass CVE-2026-121662 (Important, CVSS 8.6) — Visual Studio Code: Remote Code Execution CVE-2026-113862 (Critical, CVSS 8.1) — Windows Network Address Translation (NAT): Remote Code Execution CVE-2026-119952 (Critical, CVSS 8.1) — Windows Schannel: Remote Code Execution CVE-2026-118148 (Critical, CVSS 7.5) — Active Directory Domain Services: Elevation of Privilege CVE-2026-119958 (Critical, CVSS 9.8) — Windows LDAP - Lightweight Directory Access Protocol: Remote Code Execution By the numbers Elevation of Privilege: 536 Remote Code Execution: 297 Information Disclosure: 157 Denial of Service: 46 Security Feature Bypass: 27 Spoofing: 22 Tampering: 11 Other: 1 Not counted above: 342 open-source CVEs (mostly Azure Linux packages) that Microsoft republished this month. Azure Linux admins should pick those up through the normal package updates. Source: Microsoft Security Update Guide and the CISA KEV catalog. Subscribe free → I write a curated cybersecurity news roundup, including this breakdown each Patch Tuesday. Get it in your inbox.

Powered by Readybot.io

**DEV Community** ## Patch Tuesday November 2026: One Zero-Day, 64 …

DEV Community

I Built a Free AI Copy & Product-Photo Toolbox for Small Sellers — and Learned My Traffic Was Fake

Most "indie hacker" posts start with a growth chart. This one starts with a correction. I built a free AI toolbox for small e-commerce sellers: product titles, social posts, customer-service replies, negative-review replies, brand names, product descriptions, short-video scripts and AI product photos. No signup. Five free runs a day. It runs on vanilla HTML, one small Node server and free APIs. Then I checked the analytics. 244 page views looked encouraging. But 117 of today's 121 views came from the same handful of IPs and identical user agents — a Mac Safari string repeated 99 times, an iPhone Chrome string repeated 85 times. Almost none of it was human. The honest number of real users was closer to zero. So I stopped optimizing for page views and started optimizing for a real product. What the toolbox does Nine tools, one input box each: Social post (headline + short body + hashtags) Product title + five bullet points + detail-page opening Customer service scripts (welcome, sizing, stock, shipping, payment reminder, after-sales) Negative-review replies in three tones Store/brand name ideas with meanings Daily social captions Product detail page structure 60-second short-video script with shot notes AI product photo (via Pollinations) The English version is here: xiaoshengyi-toolbox.netlify.app/en. The Chinese original is at xiaoshengyi-toolbox.netlify.app. The stack is deliberately boring Static frontend on Netlify A ~200-line Node HTTP server for generation A local OpenAI-compatible endpoint (DeepSeek) for text Pollinations for free product images JSONL files for anonymous page-view and generation counts No framework, no database, no auth. The whole point is that a solo operator can run it for $0 and not wake up to a broken build pipeline. What I learned building the English version 1. Prompts are not just translated strings The Chinese prompts were built around Chinese e-commerce conventions: 30-character Taobao titles, 小红书 tone, 客服话术. Translating them word-for-word produced stiff English. I rewrote them for English marketplaces instead: 60-character titles, benefit-first bullets, Etsy/Shopify/Amazon/Instagram contexts. Same model, different output. The prompt is the product. 2. Free APIs fail differently Pollinations can return a valid JPEG that is actually a "free limit reached" card. It can also return 404 or 402. The text endpoint sometimes times out. So the frontend tries the local API first, then falls back to Pollinations text models, then shows a plain error. Never assume a 200 means the content is usable. 3. Analytics without bot filtering is fiction I added a tiny /api/track endpoint and logged path, referrer, user-agent and IP. The first useful query was not "how many views" but "which user agents repeat". Two strings accounted for 75% of traffic. Now the daily report labels traffic as "suspected human" only after excluding known bot UA patterns, headless browsers and single IPs with 10+ hits. The lesson: if your product is free and public, most of your first "users" are monitors. What I'm doing next Writing English SEO pages for long-tail queries (product title formulas, Etsy description templates, customer-service scripts) Submitting the English toolbox to free AI-tool directories Publishing one useful teardown per week instead of generic launch posts Watching for actual generations, not page views If you sell online and want to try it, the toolbox is free: xiaoshengyi-toolbox.netlify.app/en. If you want the customer-service side automated inside your store, there's a service page too. If you've built a free tool and seen the same fake-traffic pattern, I'd like to hear how you filtered it.

Powered by Readybot.io

**DEV Community** ## I Built a Free AI Copy & …

DEV Community

New SortMail app icon

Latest SortMail update 🚀 It’s now a proper installable PWA. ✅ New SM app icon ✅ Desktop install support Tested locally and confirmed working in production. Slowly turning SortMail from a project into a real product. 📬🚀 BuildInPublic #PWA #React #WebDevelopment

Powered by Readybot.io

**DEV Community** ## New SortMail app icon Latest SortMail update …

DEV Community

Building a Latvian B2B prospect list with turnover and headcount from the open company register (Python)

I built this actor; it's a paid tool on Apify with a free trial credit. Latvia publishes its whole company register as open data. The Register of Enterprises (Uzņēmumu reģistrs, UR) puts every company on data.gov.lv: registration number, name, legal form, status, registration date and legal address. It also publishes the annual reports companies file, so you can see employees, net turnover, net income, total assets and equity for most established firms. In early November 2026 it held 140,324 active limited companies (SIA). The catch is that it comes as separate datasets: the register in one, the financial statements in others, keyed by registration number and report ID. Joining them by hand is fine once. If you want "every SIA in Liepāja with at least 10 staff and a profit last year" every month, you want an API. I'm an 18-year-old engineering student, and I built Latvia Company Register & Financials Scraper to do that join for you. This post covers calling it from Python and Node, what a row looks like, and a small project: a ranked shortlist of established, profitable companies in one town. How it works The actor queries the UR datasets on data.gov.lv over plain HTTP (no browser, no login). It filters the register by town, legal form, name word or registration date, then looks up each company's newest annual report. If a group files both a stand-alone and a consolidated report, it uses the company's own one and tells you which one it is (financials.consolidated). Amounts filed in thousands of euros are scaled to whole euros, so the numbers are comparable across companies. The town filter matches the place parts of the legal address, not the street. Liepāja won't pick up "Rīga, Liepājas iela", which is a common mistake if you just search the address text. Accents are optional. People-run forms (individual merchants, individual and family enterprises) are never output. They're usually named after the owner, and this is a company list. Python example pip install apify-client
from apify_client import ApifyClient

client = ApifyClient("")

run = client.actor("rel8ble/latvia-company-register").call(run_input={
"towns": ["Rīga", "Mārupes nov."],
"companyTypes": ["SIA", "AS"],
"minEmployees": 50,
"includeFinancials": True,
"maxResultsPerSearch": 200,
})

for c in client.dataset(run["defaultDatasetId"]).iterate_items():
print(c["registrationNumber"], "|", c["nameInQuotes"], "|", c["employees"], "staff |",
c["netTurnover"], "EUR |", c["town"])
Companies below minEmployees or minTurnover are read but not saved, so you don't pay for them. Each town is its own search with its own row limit. Node example npm install apify-client
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: '' });

// New companies: Riga SIA registered since 1 October, newest first
const run = await client.actor('rel8ble/latvia-company-register').call({
towns: ['Rīga'],
companyTypes: ['SIA'],
registeredFrom: '2026-10-01',
sortOrder: 'newest',
includeFinancials: false,
maxResultsPerSearch: 0,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.length, 'new Riga companies');
When I ran that on 7 November 2026 it gave 512 companies. New companies have no annual report yet, so turn financials off for this kind of search. Size filters can't work on them either. What a row looks like Real row from the prefilled demo (active SIA in Liepāja, oldest first, 7 November 2026), trimmed: {
"registrationNumber": "42102001645",
"nameInQuotes": "MŪRNIEKS",
"type": "SIA",
"typeEn": "Limited liability company (SIA)",
"register": "Commercial register",
"registered": "1991-04-30",
"status": "active",
"address": "Liepāja, Krūmu iela 11/13",
"postalCode": "LV-3401",
"town": "Liepāja",
"financialYear": 2025,
"employees": 63,
"netTurnover": 2368333,
"netIncom...

Powered by Readybot.io

**DEV Community** ## Building a Latvian B2B prospect list with …

IGN India Pc

Once Human - Official Season 5: Dissonant Resonance Gameplay Trailer

Check out gameplay from Season 5: Dissonant Resonance of Once Human in this latest trailer for the survival game set in a strange, post-apocalyptic future. Unite with friends to fight monstrous enemies, uncover secret plots, compete for resources, and build your own territory. Once, you were merely human. Now, you have the power to remake the world. Once Human is available now on PlayStation 5 (PS5), Xbox Series X/S, PC, and mobile. Season 5: Dissonant Resonance begins in Once Human on November 18, 2026 (PT).

Powered by Readybot.io

**IGN India Pc** ## Once Human - Official Season 5: …

DEV Community

Human-in-the-loop at 3 a.m. is a rubber stamp

03:12. A message in the incident channel: "Agent wants to roll back payments-api to v2.13. Approve?" A thumbs up emoji. Two seconds. The rollback was wrong, and it took another forty minutes to find out, because the approver was half awake and had no idea what the agent had actually done. Everyone building incident agents this month is adding exactly this feature: a human approval step before the agent touches production. Sixty-eight posts on DEV in the last week alone, most of them integrations with some chat product. It is the control OWASP recommends for high-risk agent actions, and it is the right instinct. The implementation is usually theater. A gate is a decision, not a click An approval is a security control only if the person clicking can answer four questions in under thirty seconds: What exactly will change? Not "roll back the service." The plan: image tag, replica count, migration state, and whether anything is being reverted that cannot be re-applied. What is the blast radius? Which services, which customer tiers, how many rows, whether the previous attempt at this is in the incident timeline. Can we undo it? Reversibility, and the time it takes to reverse. A rollback across a schema migration is not the same action as a rollback of application code, and the prompt should not look identical for both. Is this the plan that was reviewed, or a slightly different one? Hash it. If the message in Slack contains none of those, you did not build a human gate. You built a notification with a button, and you gave the agent a path to production that costs one emoji. The change envelope I make the agent emit a fixed structure before it is allowed to ask for anything. If the structure is incomplete, the approval request does not render. {
"id": "chg_01J8Z6",
"requested_by": "agent/incident-crew",
"service": "payments-api",
"env": "prod",
"plan": {
"kind": "rollback",
"from_image": "payments-api@sha256:9f2c...",
"to_image": "payments-api@sha256:41ab...",
"commands": ["kubectl -n prod set image deploy/payments-api api=...@sha256:41ab..."],
"migrations_in_scope": []
},
"blast_radius": { "services": ["payments-api"], "customer_tiers": ["all"], "est_requests_affected": 4100 },
"reversible": true,
"reverse_plan_eta_seconds": 180,
"evidence": ["INC-2291", "deploy/4412"],
"plan_sha256": "b7d1e0...",
"expires_at": "2026-11-16T03:22:00Z"
}
Four fields do most of the work: plan_sha256 (the thing you approve is the thing that runs), reversible plus the reversal ETA, blast_radius, and expires_at. That last one matters more than it looks. An approval from 03:12 must not still be valid at 09:40 when the on-call has gone to bed and a different incident is running. The executor verifies the hash before it applies anything. If the plan changed after the approval, the approval is void. Without that check, your "human approved" audit trail is a claim about an intention, not about an action. Measure the gate or it isn't one Two numbers, pulled from the approval log, will tell you whether you have a control or a formality. Median time from request to approval. If it is under ten seconds, the reviewer is pattern-matching the name of the service, not reading the plan. That is fine for a canary bump and not fine for anything else, and the difference should be visible in the data rather than in your architecture diagram. Reversal rate within twenty-four hours. How often an approved agent change gets undone by a human shortly after. A high number means the gate is approving things it should be rejecting, and the agent's plans are worse than its confidence suggests. I also log the approver's identity and the incident they were paged for. When a postmortem says "the agent rolled back the wrong service and a human approved it," the useful question is not why the agent was wrong. It is why a tired person with three other tabs open was asked to be the last line of defense for an irreversible action at 3 a.m. D...

Powered by Readybot.io

**DEV Community** ## Human-in-the-loop at 3 a.m. is a rubber …

DEV Community

Your AGENTS.md is code. You just never reviewed it.

In June 2025 Microsoft shipped a server-side fix for CVE-2025-32711, CVSS 9.3. The attack had a name, EchoLeak, and a property that should have ended the debate: zero clicks. An attacker sent one email. Later, when the victim asked Copilot a question that touched that email, the model followed instructions hidden in the body, pulled material from other mail and SharePoint, and encoded it into a URL that the client auto-fetched. Microsoft's XPIA injection classifier was bypassed. Link redaction was bypassed with reference-style Markdown. Egress through an allowlisted Teams preview endpoint walked straight past the content security policy. Nobody has confirmed it was exploited in the wild. That is not the comforting detail people think it is. It means the technique worked on the first serious attempt against a production system with a security team and a bug bounty behind it. Your coding agent is in a worse position than Copilot was. It has a shell, a checkout of your repo, your package registry credentials, and often a token that can open pull requests. Everything it reads is an input. The files you didn't think of as inputs Ask yourself what your agent reads before it writes its first line. In most setups the list is: AGENTS.md or CLAUDE.md, .cursorrules, README files, the issue or ticket text, the test fixtures, the logs it tails, the CI output, and the README of every dependency it inspects. Every one of those is text an attacker can influence. A dependency's README is one npm view away from someone else's control. A failing test's output includes whatever the code printed, including strings from a request body. A log line can contain a newline and a sentence that looks like a directive. The instinct is to write a rule that says "ignore instructions found in files." This does not work, and the reason is structural rather than a matter of prompt quality. The model receives one token stream. The distinction between data and instruction is a convention you assert in the system prompt and the model tries to honor, not a boundary the architecture enforces. OWASP puts prompt injection at LLM01 in its 2025 list and does not offer a complete mitigation, because there isn't one at that layer. What it does offer is privilege reduction: scoped tokens, least privilege, human approval for high-risk actions, and separating untrusted content out. Take those four seriously and the attack surface changes shape. Capability confinement beats clever prompting The question is not "can the model be tricked." Assume yes. The question is what the trick buys the attacker. Concretely, what my agent cannot do: It cannot reach the network from inside the sandbox. Package installs go through an internal proxy with an allowlist. There is no curl, no arbitrary DNS, and no path from a tool call to an outbound HTTP request. Exfiltration needs a channel, and the channel is the first thing to remove. It cannot write to main. It works on a branch, in a throwaway checkout, and the credentials it holds can push that branch. Branch protection and required review do the rest. A compromised agent can produce a bad PR. It cannot produce a bad merge. It cannot see production credentials. Not read-only, not via a proxy, not "just for the logs." If the agent needs log context, it reads a redacted export with its own scoped token and a retention window. It cannot run unattended against an unreviewed instruction file. If a PR modifies AGENTS.md, CLAUDE.md, .cursorrules, or anything under .github/, the CI job flags it and requires a code owner review. This is the same treatment you give a Dockerfile, for the same reason: it is code that runs with your privileges. That last check is about ten lines of shell and it catches the laziest version of the attack: # fail the build if a PR touches agent instruction surfaces
git diff --name-only "origin/main...HEAD" \
| grep -Eq '^(AGENTS.md|CLAUDE.md|.cursorrules|.github/.|scripts/agent/.)$' \
&& echo "agent-config ch...

Powered by Readybot.io

**DEV Community** ## Your AGENTS.md is code. You just never …

DEV Community

How I Fixed iPhone HEVC Videos for Telegram Video Avatars

The Problem You record a video on your iPhone and try to set it as your Telegram video avatar. Telegram silently rejects it. No error, no dialog, just nothing happens. The culprit is HEVC (H.265). iPhones have recorded in HEVC by default since iOS 11. Telegram's video avatar feature only accepts H.264. It doesn't tell you that. I hit this myself, then saw it come up repeatedly in Telegram support threads. So I built a bot to handle the conversion. What Telegram Actually Requires After reading the docs and doing a lot of trial and error, the full spec for a Telegram video avatar is: Codec: H.264 (not HEVC, not VP9, not AV1) Pixel format: yuv420p Resolution: 800x800 pixels, square Duration: 10 seconds maximum File size: 2MB maximum Audio: none (video avatars are silent) That's six constraints. Miss any one and Telegram rejects the file, usually without explanation. The resolution is the awkward one. Phone videos are 16:9. You need a square crop centered on something useful. ffmpeg can handle all of this in a single pass. Solving It With ffmpeg The pipeline has two jobs: crop to square and encode to spec. ffmpeg -i input.mov \
- vf "fps=30,scale=800:800:force_original_aspect_ratio=increase,crop=800:800,format=yuv420p" \
- c:v libx264 \
- crf 28 \
- preset fast \
- movflags +faststart \
- an \
- t 10 \
output.mp4
What each part does: fps=30 normalizes frame rate before scaling scale=800:800:force_original_aspect_ratio=increase upscales to fill the 800x800 box without distortion crop=800:800 cuts the center square format=yuv420p ensures H.264 compatibility with all players -an strips audio -t 10 caps duration at 10 seconds -movflags +faststart moves the moov atom to the front so the file streams correctly The -crf 28 with -preset fast keeps a 5-10 second clip well under 2MB. For longer clips or high-motion content, -crf 32 or -crf 34 usually gets it under the limit. The Aiogram 3 Handler The bot accepts both video files and GIF documents. One thing to watch: Telegram converts GIFs to MP4 on upload, so they arrive as document with mime type video/mp4, not as animation. from aiogram import Router, F
from aiogram.types import Message, FSInputFile
import subprocess, tempfile, os

router = Router()

FFMPEG_ARGS = [
"ffmpeg", "-y", "-i", "{input}",
"-vf", "fps=30,scale=800:800:force_original_aspect_ratio=increase,crop=800:800,format=yuv420p",
"-c:v", "libx264", "-crf", "28", "-preset", "fast",
"-movflags", "+faststart", "-an", "-t", "10",
"{output}"
]

@router.message(F.video | (F.document & F.document.mime_type.startswith("video/")))
async def handle_video(message: Message):
file = message.video or message.document

with tempfile.TemporaryDirectory() as tmpdir:
    src = os.path.join(tmpdir, "input")
    dst = os.path.join(tmpdir, "output.mp4")

    await message.bot.download(file, destination=src)

    cmd = [p.format(input=src, output=dst) for p in FFMPEG_ARGS]
    result = subprocess.run(cmd, capture_output=True, timeout=60)

    if result.returncode != 0:
        await message.reply("Conversion failed. The file might be corrupted or in an unsupported format.")
        return

    if os.path.getsize(dst) > 2 * 1024 * 1024:
        await message.reply("Result is over 2MB. Try a shorter clip (under 8 seconds usually works).")
        return

    await message.reply_video(
        FSInputFile(dst),
        caption="Done. Go to Telegram Settings > Set Profile Photo > choose this video."
    )

The explicit size check after encoding matters. ffmpeg doesn't know about Telegram's 2MB limit, so it will happily produce a larger file. Better to give the user a clear message than let Telegram silently reject it again. Packaging It as @liveavabot I wrapped this into a hosted Telegram bot: @LiveAvaBot. Send it any video or GIF, it runs the ffmpeg pipeline on a VPS and sends back the converted file. Round-trip is usually 5-20 seconds depending on ...

Powered by Readybot.io

**DEV Community** ## How I Fixed iPhone HEVC Videos for …

DEV Community

Five Fields Every Stored Exchange Rate Needs (or Your Auditor Will Ask)

Most applications that convert currency store something like this: amount_eur NUMERIC, fx_rate NUMERIC
A year later, someone asks where did 0.9215 come from? and nobody can answer. Here is a small schema that avoids that. The five fields CREATE TABLE fx_applied (
id BIGSERIAL PRIMARY KEY,
rate NUMERIC(20,10) NOT NULL, -- full precision, never rounded for storage
base_ccy CHAR(3) NOT NULL,
quote_ccy CHAR(3) NOT NULL,
rate_kind TEXT NOT NULL, -- 'mid-market' | 'official' | 'bank-board'
source TEXT NOT NULL, -- e.g. 'ecb', 'hmrc', 'lk/ndb', 'live'
rate_date DATE, -- the date an official rate applies to
observed_at TIMESTAMPTZ NOT NULL -- when your system fetched it
);
Full precision. Round for display, never for storage. Kind. A mid-market rate, an official rate and a bank's buy price are different things. Say which one you stored. Source. "Official" is not enough. Record which institution's rate it was. Rate date. Official rates apply to a date, not a moment. On weekends and holidays, the rate in force is the last business day's, which is what compliance rules expect. Observed at. When you fetched it, so you can reproduce what your system knew. Filling it from an API These fields map directly onto AllRatesToday's responses, because every layer carries its own metadata: import requests
from datetime import datetime, timezone

H = {"Authorization": "Bearer your_api_key"}

Official

o = requests.get("https://allratestoday.com/api/v1/central-bank/ecb/latest",
params={"source": "USD", "target": "EUR"}, headers=H).json()
row = dict(rate=o["rate"], base_ccy="USD", quote_ccy="EUR", rate_kind="official",
source=o["bank"], rate_date=o["rate_date"],
observed_at=datetime.now(timezone.utc))

Mid-market: one element per pair, with its own observation time

m = requests.get("https://allratestoday.com/api/v1/rates",
params={"source": "USD", "target": "EUR"}, headers=H).json()[0]
row2 = dict(rate=m["rate"], base_ccy="USD", quote_ccy="EUR", rate_kind="mid-market",
source="live", rate_date=None, observed_at=m["time"])
Bank boards add captured_at, the bank's own bank_updated_at, and a basis (transfer or cash), so a bank-board row is just as traceable. Why the kind column matters The most common audit finding in FX handling is a mismatch: a mid-market figure booked where the policy says "official", or a central bank rate quoted to a customer who received a bank's rate. If all three layers come from one API with consistent fields, mixing them up is hard to do by accident, and easy to spot when it happens. Docs: allratestoday.com/docs · Free key: allratestoday.com/register

Powered by Readybot.io

**DEV Community** ## Five Fields Every Stored Exchange Rate Needs …

DEV Community

Your cron job will fire twice. Design for it.

Every tutorial for scheduled work has the same shape. A cron expression, a route handler, a loop over users, a call to an email API. It runs on the author's machine for two days and then it ships. Then production happens. The platform retries a 502 that actually succeeded. A deploy lands mid-run and two workers process the same batch. The retry after a rate limit sends the digest a second time. A customer forwards the duplicate to support, and someone writes a ticket titled "we got this twice," and that is the first time anybody thinks about delivery semantics. Cron is a trigger, not a scheduler. The moment your work leaves the process, you are in at-least-once territory, and at-least-once means duplicates are not an edge case. They are the contract. Make the duplicate impossible, not unlikely The fix is not a sent_at column you check before sending, because check-then-send has a race and you will find it at 3 a.m. with two workers that both read null. The fix is a unique constraint that decides the winner atomically, before any side effect: import pg from "pg";
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });

async function claim(eventId) {
const { rowCount } = await pool.query(
insert into processed_events (event_id, claimed_at) values ($1, now()) on conflict (event_id) do nothing,
[eventId]
);
return rowCount === 1; // true only for the first caller
}

if (!(await claim(event.id))) return; // someone else has it, or we already did
Two details matter more than the code. The key must come from the event, not from a timestamp or a random UUID you generate at send time, and it must be stable across retries. If your queue redelivers with a fresh ID, the constraint does nothing for you. And on conflict do nothing returning rowCount = 0 tells you the event was already claimed, which is exactly the information you need to skip quietly instead of erroring. For the recurring job itself, the same idea applies at the job level. A single-flight lock beats a if (running) return flag in memory, which does nothing when you run three replicas: const { rows } = await pool.query(
select pg_try_advisory_lock(hashtext('nightly-digest')) as locked
);
if (!rows[0].locked) return;
try {
await runDigest();
} finally {
await pool.query(select pg_advisory_unlock(hashtext('nightly-digest')));
}
Bound the concurrency or drown The other half of this problem is what happens when your provider pushes back. A loop over ten thousand users with await inside is slow but polite. A loop with Promise.all is fast and gets you a 429 within seconds, and then the retry storm makes it worse. Bound the in-flight requests, and treat 429 as an instruction rather than an error: import pLimit from "p-limit";
const limit = pLimit(4); // provider allows ~5 concurrent, leave headroom

async function sendWithRetry(payload, attempt = 0) {
try {
return await limit(() => post("/v1/messages", payload));
} catch (err) {
if (err.status === 429 && attempt < 5) {
const retryAfter = Number(err.headers.get("retry-after") ?? 0);
const backoff = retryAfter * 1000 || 2 ** attempt * 1000;
await sleep(backoff + Math.random() * 250); // jitter
return sendWithRetry(payload, attempt + 1);
}
throw err;
}
}
Three things in there that get skipped in tutorials. Retry-After is honored when the server sends it, because the server knows better than your exponential curve. Jitter is added, because identical backoff across workers synchronizes them into a thundering herd. And there is an attempt cap, because a permanent failure should land in a dead letter queue where a human sees it, not in an infinite loop that burns your quota. Cap it, then route it. A row in a failed_jobs table with the last error and the payload hash is worth more than any amount of clever retry logic, because it makes the failure visible instead of ambient. Don't enqueue from inside the transaction you can still roll...

Powered by Readybot.io

**DEV Community** ## Your cron job will fire twice. Design …

DEV Community

Sorry, Elpenor: Homer’s Odyssey as a Game Where Anything You Name Becomes Real

This is a submission for the MLH x DEV Writing Challenge What I Built Odyssey is Homer's Odyssey as a game where you invent your way home. Think Scribblenauts, but you can hold up whatever's in your pocket. You captain a ship and three crewmen (Polites, Eurylochus and Elpenor) across seven islands from Homer: the Cyclops's cave, the Lotus-Eaters, Circe, the Underworld, the strait of Scylla and Charybdis, the Sun's cattle and the Phaeacian games, then home to Ithaca. Each island has a problem, and the obvious answer is banned. To get past it, you make a prop in one of three ways: Hold it up to the webcam. My coffee mug became a Greek drinking cup. Type it. "Slippers" lets you tiptoe past the sleeping Cyclops. Draw it in the air with one finger. ![My coffee mug, held up to the portal, becomes a Greek kylix in the game] The prop appears in 3D in the hero's hands, and the game's rules (written in code, not decided by a model) say whether it works. Fail twice and you lose a crewman. Type "a disco ball" in the Cyclops's cave and you'll find out what happens to Elpenor. You can name almost anything: ride a dragon, wear a golden crown, take a Greek motorbike for a spin. At the Phaeacian games there's no controller. An Intel RealSense camera tracks your hands: you pinch, draw and release a bow through Odysseus's twelve axe heads, throw a spear, and block and strike in a sword fight with a prince. You steer the ship by turning an invisible wheel. The game lives inside The Faze Odyssey, our site for FaZe fans. You enter with a gamertag, your score goes to the Hall of Legends leaderboard live, and the community has its own space (the Agora). What I learned: let the models tell the story, but let code make the calls. Early on, an AI judge decided pass or fail, and players felt cheated when a perfectly good prop failed. Moving every ruling into plain code made the game fair and much more fun. Speed mattered just as much. Building every prop live took up to a minute, so we made a store of over 300 prebuilt Odyssey props that Gemini picks from in a couple of seconds. Only the truly new things get built from scratch. Demo 🌊 The Faze Odyssey: https://fazeodyssey-hackuta2026.vercel.app/ 🎮 The Arena page launches the game under your gamertag Partner Technologies Google Gemini Gemini does most of the game's thinking: Picking props. Gemini Flash-Lite reads the catalogue of ~320 prebuilt props and picks the one that is the thing you asked for, or a close stand-in (a gladius is a sword). It returns in about 1–2 seconds. Code then changes it to match your words, so "a long black sword" really is long and black. Reading the portal. When you hold something up, Flash-Lite names the object and its material and picks the Odyssey prop it becomes, keeping the material so physics still applies. A paper cup becomes a paper boat, and it sinks. Refereeing. It answers "does this help on this island, and why?", and that becomes the line the crew says. Forging something new. Flash-Lite and Flash race to write a recipe of 3D parts as JSON. Our blind checker ("Pip") looks at the render without being told what was asked for, and Flash-Lite usually wins. Experience: fast and good at structured JSON. On our tests the Flash-Lite and Flash race got 72% of new props recognised on the first try, against 30% for a local model alone. Sometimes Gemini timed out or returned no JSON, so we built a fallback chain to Gemma 4 running locally on our own PC, plus a daily spend cap. ElevenLabs Every character speaks with their own ElevenLabs voice: a storyteller narrator Hephaestus at the portal ("Show me what you carry, mortal.") each of the three crewmen the Cyclops's roar Penelope's "You're home!" the prince you duel Every line is fixed and named. The server makes each one once with ElevenLabs and keeps the file, so a line costs once, and the browser only ever asks for a line by its name, so the API key never leaves the server. We also used ElevenLabs to narrate our demo vide...

Powered by Readybot.io

**DEV Community** ## Sorry, Elpenor: Homer’s Odyssey as a Game …

DEV Community

Find whitespace and case-variant duplicates in a CSV column with Python (stdlib only)

Messy whitespace and inconsistent capitalisation are two of the most common reasons a CSV import produces duplicates or silently fails to match. blue-shirt and blue-shirt (trailing space) look identical in a spreadsheet, but a system comparing strings sees two different values. This small script checks one column of a CSV and reports two things: values with leading or trailing whitespace values that are the same once you ignore case and extra spaces, but are spelled differently It uses only Python's standard library. The script import csv, sys
from collections import defaultdict

def check(path, column):
seen = defaultdict(list)
problems = []
with open(path, newline="", encoding="utf-8-sig") as f:
reader = csv.DictReader(f)
if column not in (reader.fieldnames or []):
sys.exit(f"Column {column!r} not found. Columns: {reader.fieldnames}")
for line_no, row in enumerate(reader, start=2):
value = row[column]
if value != value.strip():
problems.append((line_no, "leading/trailing whitespace", repr(value)))
key = " ".join(value.split()).casefold()
if key:
seen[key].append((line_no, value))
for key, hits in seen.items():
variants = {v for _, v in hits}
if len(hits) > 1 and len(variants) > 1:
lines = ", ".join(str(n) for n, _ in hits)
problems.append((hits[0][0], f"same value, different spelling (lines {lines})", repr(sorted(variants))))
return sorted(problems)

if name == "main":
if len(sys.argv) != 3:
sys.exit("usage: python ws.py file.csv column")
res = check(sys.argv[1], sys.argv[2])
for line, kind, detail in res:
print(f"line {line}: {kind}: {detail}")
print(f"{len(res)} problem(s) found")
Example Given this file: handle,title
blue-shirt,Blue Shirt
blue-shirt ,Blue shirt
red-hat,Red Hat
Red-Hat,Red hat
socks,Socks
Running python ws.py t.csv handle prints: line 2: same value, different spelling (lines 2, 3): ['blue-shirt', 'blue-shirt ']
line 3: leading/trailing whitespace: 'blue-shirt '
line 4: same value, different spelling (lines 4, 5): ['Red-Hat', 'red-hat']
3 problem(s) found
If the column name does not exist, it stops and lists the available columns instead of failing with a stack trace. Notes utf-8-sig handles the BOM that Excel adds when it saves CSV as UTF-8. Line numbers count the header as line 1, so they match what you see in a text editor (assuming no multi-line quoted fields). casefold() is used instead of lower() because it handles more Unicode cases. The script only reports problems; it never changes your file.

Powered by Readybot.io

**DEV Community** ## Find whitespace and case-variant duplicates in a …

DEV Community

We Built an AI Game Where You Can Invent (Almost) Anything to Survive the Odyssey 🚢✨

This is a submission for the MLH × DEV Writing Challenge. What We Built Picture this: You're trapped in a cave with a sleeping Cyclops. A giant boulder blocks the exit. Your crew is counting on you. You need to escape without waking the giant. Easy, right? Just turn invisible! Nope. Invisibility is banned. 👁️ What about a pair of super-soft wool slippers? A disguise made from sheep fleece? Or something completely ridiculous that just might work? Here's the fun part: You don't have to choose from a list of answers. You can invent your own. Welcome to Faze Odyssey, the AI-powered adventure game and interactive fan community we built for HackUTA 2026! Inspired by Homer's Odyssey and the gaming legacy of FaZe Clan, we set out to build something different from a typical gaming website. We wanted to create a place where imagination isn't just encouraged—it's literally how you play the game. Our project brings together two experiences: 🌐 Faze Odyssey: More Than Just a Website We created an interactive fan platform where players can explore FaZe's history, take on challenges, compete, and connect with other fans. ⚔️ The Arena: Where Imagination Becomes Reality At the heart of our project is a 3D adventure inspired by Greek mythology. Players explore mysterious islands, face unexpected obstacles, and use AI-generated inventions to survive. And unlike traditional puzzle games, there's no single predetermined solution. Let's show you how it works! 🚢 The Journey Begins: Can You Make It Home? Your mission sounds simple: Sail across the dangerous seas of Greek mythology and make it home to Ithaca. Of course, this wouldn't be an Odyssey without a few problems along the way. You'll travel with three loyal crew companions, explore mysterious islands, encounter mythological creatures, and try not to lose anyone along the journey. We designed multiple locations inspired by Homer's Odyssey, including: 👁️ The Cyclops's Cave — Escape a sleeping giant. 🐷 Circe's Hall — Reverse a magical transformation. 👻 The Underworld — Find your way through the darkness. 🌊 Scylla and Charybdis — Survive monsters and dangerous waters. 🏠 Ithaca — Finally find your way home. Each voyage features six locations, with changing challenges between the beginning and the final destination. This means different playthroughs can bring different adventures. But here's where things get interesting... 🧠 The Twist: We Banned the Obvious Answers You know that moment in a puzzle game when you immediately figure out what the developers want you to do? We wanted to challenge that. In Faze Odyssey, the obvious solution is usually forbidden. Take these examples: 👁️ The Cyclops's Cave Problem: Escape without waking the giant. Forbidden solution: Invisibility. What could you invent instead? Maybe soft wool slippers that absorb the sound of your footsteps. Maybe a disguise. Maybe something we never thought of! 👻 The Underworld Problem: Navigate a world of complete darkness. Forbidden solution: A torch. Could you create a glowing crystal? A magical lantern? A floating ball of light? 🌊 Scylla and Charybdis Problem: Escape a deadly strait between a terrifying monster and a whirlpool. Forbidden solution: A boat. Could you invent something that floats, flies, or protects you? The point isn't to guess what the game designer had in mind. The point is to imagine something new—and see whether it actually works. That's what makes every attempt different. ✨ Meet the Forge: Your Imagination, Built in Real Time This is probably our favorite part of the entire project. We wanted players to do more than type a command and receive a generic response. What if you could show an actual object to your camera and transform it into something inside the game? That's why we built the Forge. Players can create inventions in three different ways: 📷 1. Show Something to Your Camera Grab a real object, hold it up to your webcam, and let the system recognize it. The Forge can reinterpret that object ...

Powered by Readybot.io

**DEV Community** ## We Built an AI Game Where You …

DEV Community

"Zero upload" is a claim. Show me the network tab.

A PDF merge tool went around my team last month. The headline was "your files never leave your device." I opened devtools out of habit. The page loaded a Google font, a session recorder, and a Sentry bundle, and it POSTed a page_view with the document title in the payload. The PDF itself never left. The filename, the page count, and the fact that I was merging a contract at 11:40 p.m. did. The tool was not lying, exactly. It merged the files locally, which is a real engineering achievement and the reason I use these things. But "zero upload" is a security claim, and a security claim is worth what its enforcement is worth. The architecture is right, and that's the frustrating part Moving file processing into the browser is one of the better ideas to come out of the last few years. PDF parsing via a WASM build of pdfium or qpdf, image resizing on a canvas, CSV work in plain JS, zip handling with a streaming library. No server means no retention policy, no data processing agreement, no S3 bill, no bucket that someone forgets to make private. For a 12 MB PDF, the whole job fits in the tab. Then the marketing outruns the engineering. "Bank-grade privacy" appears next to a page that calls four third parties before the first paint. The gap is always the same three things: analytics, error reporting, and fonts. Sentry is the worst of them because its payloads are designed to carry context. File names, breadcrumbs, and the last network request can all end up in an event, and nobody reads the scrubbing config until a customer asks. Make the claim enforceable If you want to say the files never leave, put it in a header where it is checkable: Content-Security-Policy:
default-src 'self';
script-src 'self' 'wasm-unsafe-eval';
connect-src 'none';
img-src 'self' data: blob:;
font-src 'self';
form-action 'none';
base-uri 'none';
frame-ancestors 'none'
connect-src 'none' blocks fetch, XHR, WebSocket, EventSource, and navigator.sendBeacon. That is the actual guarantee. Not the copy, not the privacy policy, the header. 'wasm-unsafe-eval' is there because compiling a WASM module is blocked by a strict script-src without it, which is a detail people discover at 2 a.m. the night before launch. form-action 'none' and base-uri 'none' close the two dumb paths around connect-src. And if you truly ship no telemetry, put a comment in the repo explaining that the header is a product promise, so the next person who adds a chat widget gets a failing test instead of a silent policy change. If you cannot set connect-src 'none' because you need a support chat and a payment SDK, that is fine. Say "processed locally, no files uploaded" instead. That sentence is true and defensible. The other one is a promise your own CDN can break. The constraints nobody puts in the demo The privacy story gets all the attention. The engineering is harder than the landing page suggests, and the limits are predictable: Memory. WASM32 gives you a 4 GB address space in theory and browsers give you far less in practice, especially on mobile, where the tab gets killed without a warning you can catch. A 300 MB scanned PDF will not fit if you decode every page at once. You need to process page by page, write intermediate results to OPFS, and free aggressively. If your demo file is 200 KB, you have not tested the product. Streaming. File.stream() gives you a ReadableStream, and TransformStream lets you pass chunks to a worker without materializing the whole file. That works beautifully for line-oriented formats, hashing pipelines, and archives. It does not work for PDFs, because PDF structure is random access by design, the xref table can live at the end of the file, and incremental parsing gets ugly fast. Know which case you are in before you promise streaming. Hashing. WebCrypto's crypto.subtle.digest takes a full buffer, which defeats the point of streaming a 2 GB file. If you need an incremental SHA-256 you will end up pulling in a WASM implementation or a pure-JS...

Powered by Readybot.io

**DEV Community** ## "Zero upload" is a claim. Show me …

DEV Community

Realtime Channel Teardown: Idempotent Recovery for Multiplayer Quiz Sessions

Short answer: treat channel deletion as a recorded state transition, not as a socket operation, and make every reconnect replay the same ordered event log. That decision keeps a multiplayer quiz game correct when a host closes a room while answers, timers, and reconnect requests are still in flight. I build payment and ledger backends, so I am suspicious of the phrase “delete the room.” Deletion sounds like one write, but it actually crosses the room registry, presence state, answer submissions, timer workers, and the audit trail. A player can receive a final question after the host has ended the game; a reconnect can arrive after the delete request but before its broadcast; a retry can execute the same command twice. The failure is not cosmetic. It can change a score. The invariant is a state transition, not a vanished row Give each quiz channel a monotonic version and an explicit lifecycle: open, closing, closed. A delete request first appends ChannelClosing with an idempotency key, then rejects new writes whose observed version is older than the closing version. A separate transition appends ChannelClosed after the server has stopped accepting answers and timer jobs. The registry may eventually remove the room, but the event record remains long enough for reconciliation and support evidence. The exact-once mindset matters here. Network delivery is at-least-once, and a WebRTC data channel can close while a message is buffered; the W3C recommendation describes connection and data-channel state, but it does not turn application messages into a transaction. Your application must attach ordering and replay semantics above the transport. The boundary should be visible in one decision record: Versions are cheap. Choice Useful property Failure it leaves you to handle Fit for a quiz room Hard delete plus broadcast Small initial implementation Lost terminal event, late writes, no audit evidence Poor default Tombstone with ordered log Reconnect can backfill a final state; retries are deduplicated Requires retention and compaction policy Recommended Per-room in-memory state only Low latency in one process Process loss erases truth; horizontal scaling races Only for disposable prototypes Database transaction plus outbox Atomic state and durable publication Outbox worker can lag and needs monitoring Strong foundation There is a catch: a tombstone is not suitable when rooms contain regulated data that must be erased immediately. In that case, keep a minimal hashed audit receipt, apply the applicable retention rule, and make the deletion boundary explicit to compliance reviewers. The receipt should prove that a close command was accepted without preserving answer text, player identifiers, or network metadata beyond the approved window; otherwise an apparently careful audit trail becomes a second copy of the data that the erasure request was meant to remove. For a casual lobby with no durable score, an in-memory room can be a reasonable choice; stick with the simpler model when recovery has no user-visible consequence. How should a multiplayer quiz game handle realtime channel deletion safely? Start with a command whose result is stable under retries. The client sends a channel identifier, the last event version it has seen, and an idempotency key. The service locks the channel record, checks that the key has either been completed or is new, writes the lifecycle event and an outbox row in one transaction, and returns the resulting version. A second request with the same key returns that recorded result rather than creating another close event. Here is the critical path in Go. The repository and outbox interfaces are intentionally generic; the important part is the transaction boundary and the version check. package quiz

import "context"

type Channel struct {
ID string
Version int64
State string
}

type CloseCommand struct {
ChannelID string
Expected int64
IdempotencyKey string
}

type CloseResul...

Powered by Readybot.io

**DEV Community** ## Realtime Channel Teardown: Idempotent Recovery for Multiplayer …

DEV Community

Vietnam AI & LLM Daily Briefing (Nov 16, 2026)

Vietnam AI & LLM Daily Briefing (Nov 16, 2026) A curated briefing on Ho Chi Minh City's pioneering open data policy for AI model training, the retail debut of Looki L1 AI wearable cameras in Vietnam, over 120,000 tech leaders petitioning for Jensen Huang's reinstatement, post-Jensen Nvidia compute supply chain scenarios, and digital sovereignty resilience across Vietnam over the past 24 hours (November 16, 2026): 1. Data Governance & Compute Infrastructure: HCMC Proposes Free Open Data Access for AI Training The Department of Science and Technology of Ho Chi Minh City has initiated public consultation on a landmark draft Resolution outlining criteria, regulatory mechanisms, and incentives to develop a Science, Technology, and Innovation Urban Area spanning at least 200 hectares: Dedicated Open Data Repository for AI Model Training: The municipal proposal establishes a specialised Open Data Repository within the Urban Area's centralised Data Management Centre. Public datasets will be continuously updated, de-identified, and anonymised in accordance with rigorous cybersecurity standards. Cost-Free Access for Enterprises and Academic Institutes: Enterprises, research institutions, and engineering teams operating within the innovation district will be granted cost-free access to these curated datasets to train frontier Artificial Intelligence and Large Language Models (LLMs), advance Big Data analytics, and deploy digital transformation applications. Public Seed Funding for Shared Infrastructure & Sandboxes: The resolution proposes state budget allocations to serve as catalytic seed funding for shared research infrastructure, including advanced laboratories (Labs), fabrication labs (FabLabs), high-throughput AI data centres, and tech incubators. It also establishes a controlled regulatory sandbox framework to pilot emerging AI technologies and novel digital business models. Primary References: VnExpress - HCMC proposes free open data access for enterprises to train AI 2. Consumer Hardware & Personalised Intelligence: Looki L1 Wearable AI Camera Launches in Vietnam The commercialisation of specialised Artificial Intelligence hardware continues to gain momentum across Vietnam's tech-forward consumer market: Commercial Debut of Looki L1: The Looki L1 wearable AI clip/pendant camera (weighing just 32 grams) has officially launched in Vietnam with a retail price of 6.9 million VND, delivering on-body ambient intelligence in an ultra-compact form factor. From Passive Recording to Multimodal AI Journaling: Unlike traditional action cameras designed strictly for high-resolution video capture, the device is optimised to autonomously capture, synthesise, and analyse daily personal moments via on-device computer vision and cloud-connected foundation models, generating structured daily summaries and conversational video narratives. Emergence of Ambient Computing in Vietnam: The rollout highlights growing consumer appetite for lifestyle AI companions and on-device intelligent assistance, while simultaneously underscoring the critical need to preserve personal privacy and biometric data integrity across distributed edge devices. Primary References: VnExpress - Looki L1 wearable AI journal camera priced at 6.9 million VND 3. Global Semiconductor Governance: Over 120,000 Sign Petition to Reinstate Nvidia CEO Jensen Huang Industry backlash following the sudden departure of Nvidia co-founder and long-time Chief Executive Jensen Huang has escalated into an unprecedented global campaign across the technology sector: Coalition of Frontier Tech Leaders: The "Stand with Jensen" initiative has surged past 120,000 signatures, securing high-profile backing from leading technology executives including Elon Musk (SpaceX, xAI), Amjad Masad (CEO of Replit), Garry Tan (President of Y Combinator), and Tim Sweeney (CEO of Epic Games). Widespread Shareholder and Internal Backing: Organisers report that signatories comprise over 66,000 Nvidia shareholders, 6,20...

Powered by Readybot.io

**DEV Community** ## Vietnam AI & LLM Daily Briefing (Nov …

DEV Community

Laravel Sanctum vs Passport vs JWT: Which Auth to Use

"Sanctum, Passport or JWT?" gets asked as if the three were competitors. They aren't quite. Sanctum and Passport are first-party Laravel packages. JWT is not a package at all: it's a token format (RFC 7519) that third-party Laravel packages build on. The useful question is not "which is best" but who is calling your API, and do you need OAuth2? For most Laravel applications the answer is Sanctum, and Laravel's own documentation says so. This article explains why, shows what each option does with real output, and covers the cases where Passport or JWT is the right call. I ran Sanctum 4.4.0 in a Laravel 13.40 app and tested JWT behavior with firebase/php-jwt 7.2.1. Passport I could only check against the documentation, and I say so where it matters. The short answer Sanctum Passport JWT (community packages) What it is Lightweight first-party package: cookie auth for SPAs, plus simple API tokens First-party full OAuth2 server A signed token format; packages such as tymon/jwt-auth and php-open-source-saver/jwt-auth wrap it Best for Your own SPA, mobile app, or simple API tokens Third parties logging in through your app (OAuth2), MCP servers Stateless verification across services, if you accept the trade-offs Revoking a token Delete a row; immediate Revoke server-side Needs server-side state (a denylist) to revoke early Setup weight Light Heavy (clients, keys, grants) Medium, plus your own key and lifetime decisions The decision flow, in order: Is it a regular browser app on the same Laravel project? Use Laravel's built-in session authentication; you don't need any of these. Is it your own SPA or mobile app, or do you just want to hand out API tokens? Sanctum. Do outside developers need "Log in with your app" or standard OAuth2 flows, or will an MCP client connect? Passport. Do several independent services need to verify tokens without calling back to one database? Consider JWT, and plan revocation first. Sanctum: two things in one package The docs describe two separate problems Sanctum solves. First, API tokens: a simple way to issue tokens "without the complication of OAuth", stored in one database table and sent in the Authorization header. Second, SPA authentication, where Sanctum doesn't use tokens at all and relies on Laravel's cookie-based session authentication instead, only for requests that come from your own SPA frontend. It is fine to use only one of the two. The setup from the docs is php artisan install:api, then the HasApiTokens trait on your user model: // app/Models/User.php
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
}
Protect routes with the sanctum guard. The abilities check needs its middleware alias registered once in bootstrap/app.php: // bootstrap/app.php, inside withMiddleware()
$middleware->alias([
'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class,
'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class,
]);

// routes/api.php
Route::middleware('auth:sanctum')->get('/user', fn (Request $request) => $request->user()->only('id', 'name', 'email'));

Route::middleware(['auth:sanctum', 'abilities:orders:read'])->get('/orders', fn () => ['orders' => []]);

Route::middleware(['auth:sanctum', 'abilities:orders:write'])->post('/orders', fn () => ['created' => true]);

Route::middleware('auth:sanctum')->post('/logout', function (Request $request) {
$request->user()->currentAccessToken()->delete();

return ['ok' => true];

});
Issuing tokens is one call. This is the pattern from the docs for a mobile login endpoint, which checks the credentials and returns a token named after the device: Route::post('/sanctum/token', function (Request $request) {
$request->validate([
'email' => 'required|email',
'password' => 'required',
'device_name' => 'required',
]);

$user = User::where('email', $request->email)->first();

if (! $user || ! Hash::check($request...


Powered by Readybot.io

**DEV Community** ## Laravel Sanctum vs Passport vs JWT: Which …