AI 资讯
How a Single beforeEach Killed Our CI for 36 Hours
Six failed CI runs. Thirty-six hours of GitHub Actions time. Every run timing out at exactly the 6-hour limit. The culprit was one line in tests/setup.js . The Setup We were building a multi-tenant platform with a PostgreSQL backend — around 76 database models handling everything from user accounts and billing to visitor logs and real-time notifications. The test suite had grown to roughly 1,140 test cases across 36 files. Standard stuff. CI ran on every PR. Tests passed locally. And then one day, CI just... never finished. The Anti-Pattern Here's what the test setup looked like: // tests/setup.js beforeEach ( async () => { const tableNames = await getTableNames (); // 76 tables await sequelize . query ( `TRUNCATE TABLE ${ tableNames . join ( ' , ' )} CASCADE;` ); }); The intent was clean isolation — every test starts with a blank slate. Reasonable in theory. Catastrophic in practice. The Math Do the multiplication: 76 tables × 1,140 tests = 86,640 TRUNCATE operations Each TRUNCATE TABLE ... CASCADE is not a cheap operation. PostgreSQL has to: Acquire exclusive locks on all referenced tables Walk the foreign key graph to find dependent tables Truncate each in dependency order Release locks With a moderately complex schema where most tables reference others (users → societies → members → invoices → payments → ...), a single TRUNCATE ... CASCADE on a central table can fan out into dozens of implicit truncations. Multiply that by 86,640 and you have a test suite that will never complete within any reasonable timeout. Why It Wasn't Caught Sooner Two reasons: 1. It used to be fast. When the suite had 50 tests and 20 tables, this pattern worked fine. 50 × 20 = 1,000 truncations — uncomfortable but survivable. Nobody noticed when the suite crossed a tipping point. 2. Local runs used a different database state. Locally, developers often ran a subset of tests with --grep or file-specific runs. The full suite was only ever run on CI, and CI was slow enough that most assumed i
AI 资讯
The x-tenant-id Pattern: Multi-Tenant API Without Multi-Tenant Complexity
When you're building a multi-tenant SaaS, the first architectural question is usually: how do you keep tenant data isolated? The options range from separate databases per tenant (maximum isolation, maximum cost) to a shared database with row-level filtering (minimum cost, more careful coding required). But there's an equally important question that gets less attention: how does your API know which tenant context a request belongs to? This post covers a pattern we've used in production: a custom request header for tenant scoping, combined with JWT authentication. Simple to implement, easy to audit, and flexible enough to support multi-tenant access from a single user account. The Three Common Approaches 1. Subdomain-based ( tenant.yourdomain.com ) The tenant is encoded in the hostname. Each subdomain routes to the same backend, which extracts the tenant from the Host header. Good: Intuitive, visible in the URL. Bad: Requires wildcard TLS certs, more complex DNS setup, awkward in development, doesn't work for mobile API clients the same way. 2. URL path-based ( /api/tenants/{tenantId}/... ) The tenant identifier is part of every route path. Good: RESTful, self-documenting. Bad: Bloats all route definitions, requires every endpoint to include the tenant segment, makes API versioning messier. 3. Header-based ( x-tenant-id: <id> ) A custom header carries the tenant context. Routes stay clean. The tenant scope is resolved in middleware before the handler runs. Good: Routes stay simple, middleware handles scoping uniformly, works well with JWT auth, easy to test. Bad: Less visible (the tenant isn't in the URL), requires clients to always include the header. We use the header approach. The Implementation The API accepts two forms of auth: A JWT token in the Authorization header — identifies who is making the request A tenant ID in the x-tenant-id header — identifies on behalf of which tenant POST /api/v1/members Authorization: Bearer eyJhbGciOiJIUzI1NiIs... x-tenant-id: ten
AI 资讯
onPreviewKeyEvent vs onKeyEvent on Android TV: A Subtle D-Pad Bug
The bug report was simple: long-pressing the d-pad center button on a channel card should toggle the favorite — it wasn't working reliably. On some devices it fired once and stopped. On others it didn't fire at all on long-press. The fix was changing onKeyEvent to onPreviewKeyEvent in one Composable. The reason why is worth understanding. Background: Two Event Handlers in Compose Jetpack Compose exposes two modifier-level hooks for key input: Modifier . onKeyEvent { keyEvent -> .. . } Modifier . onPreviewKeyEvent { keyEvent -> .. . } They sound equivalent. They're not. The difference is where they sit in the event propagation chain . How Android TV Routes D-Pad Events When a user presses a key on a TV remote, Android routes the event through a dispatch tree: Activity └─ ViewGroup (root) └─ FocusedComposable └─ Child Composables The event travels down first (capture phase), then up (bubble phase): Capture (top → focused node): onPreviewKeyEvent handlers fire here, outermost first. Bubble (focused node → top): onKeyEvent handlers fire here, innermost first. onPreviewKeyEvent is the capture phase. onKeyEvent is the bubble phase. Why This Matters for Long-Press Android TV handles long-press recognition at the framework level. When you hold the d-pad center button: A KeyEvent.ACTION_DOWN fires immediately. If the key is held, the framework generates repeated ACTION_DOWN events at the key repeat rate. ACTION_UP fires when the button is released. The long-press callback that Compose's focus system uses for "confirm" actions (select, activate) consumes ACTION_DOWN during the bubble phase — specifically to prevent the holding action from also triggering the tap action. When the ChannelCard had a click handler wired for the primary action and onKeyEvent for the long-press toggle, the click handler's bubble-phase consumption of ACTION_DOWN was racing with the long-press handler. On some devices the click handler won, swallowing the event before the long-press code ran. The Fix
AI 资讯
Mono-Repo + Multi-Repo: How We Structured 6 Apps Across 4 Repositories
Most teams treat "monorepo vs multi-repo" as a binary choice. Pick one, commit, move on. We ended up with a hybrid, and it turned out to be the right call — not out of indecision, but because our apps have genuinely different deployment and ownership characteristics. Here's what we built, why, and what it costs. The System The platform consists of six applications: App Type Primary Users REST API backend Node.js + TypeScript — (consumed by all apps) Society dashboard React web app Society managers, admins, accountants Company admin panel React web app Internal operations Marketing website Next.js Public Resident mobile app React Native (Expo) Residents Guard mobile app React Native (Expo) Security personnel All six apps talk to the same API. But they have very different deployment cycles, team ownership, and testing requirements. The Structure: 4 Repositories repo: main-platform (monorepo) ├── api/ — Express + Prisma backend ├── web-society/ — Society dashboard ├── web-admin/ — Company admin panel └── web-marketing/ — Marketing site repo: mobile-resident — Resident app (React Native) repo: mobile-guard — Guard app (React Native) repo: mobile-staff — Society staff mobile app (React Native) The web apps and the API live together in one monorepo. The three mobile apps each have their own repository. Why Split Mobile From Web? The driving factor was deployment cadence and review process . Web apps deploy on push — merge to main, CI builds, CDN updated within minutes. The feedback loop is fast, rollbacks are instant, and there's no approval gate between code and production. Mobile apps go through app store review. A release cycle includes building a release APK, submitting to Google Play (and Apple App Store), waiting for review, and then a staged rollout. The cadence is measured in days, not minutes. Mistakes are expensive to reverse — a bad release means submitting a patch, waiting again, and potentially having a broken version live for days. Given that difference, mob
AI 资讯
I Built a Manga Reader That Works on Every Platform --Here's How
I Built a Manga Reader That Works on Every Platform — Here's How Nyora is a free, open-source manga/manhwa/manhua reader for Android, iOS, macOS, Windows, Linux, Web, and even Docker — with AI-powered on-device translation and cross-platform sync. The Problem Every manga reader makes you choose: Free but ad-riddled (most Android readers) Polished but paywalled (commercial apps) Powerful but single-platform (Tachiyomi, Aidoku) I wanted one library — same titles, same progress, same bookmarks — on my phone, laptop, and browser. No ads. No account required. So I built it. What Nyora Does Every Platform, One App Platform Distribution Android APK (sideload) iOS/iPadOS IPA via AltStore/SideStore macOS .dmg or brew install --cask nyora Windows .exe (x64 + ARM64) Linux .deb , .rpm , or curl installer Web web.nyora.xyz — zero install Docker Single container, self-hosted No account needed to read. Cloud sync is opt-in. AI Translation That Understands Manga This is the flagship feature. Instead of dumping translated text over the artwork: Detects text baked into speech bubbles and captions Translates using on-device ML Typesets the result back over the original artwork Each platform uses the best local engine: Android : Google ML Kit + ONNX Runtime iOS : Apple Intelligence + Google Translate macOS : Apple Vision + MangaOCR CoreML Windows : Windows OCR Linux : Tesseract There's also an Ensemble AI Narrative Engine that tracks character names and speaking styles across chapters so translations stay consistent. 1,100+ Sources The Android app pulls from 1,100+ manga sources via 35 generic engine templates (Madara, FoolSlide, MMRCMS, etc.). Web has ~390 live, health-checked sources. Desktop ports are growing toward parity. Free Cloud Sync Sync library, categories, reading history, bookmarks, and exact page progress across all six platforms. Two sign-in methods: Google OAuth Nyora Cloud (email + password, free) Self-hostable — the backend is just Supabase/PostgreSQL with row-level s
AI 资讯
Dead-Letter Queues for LLM Extraction Failures: Capture, Triage, and Replay Without Losing Trust
A validation failure is not an exception to hide. It is a record your system does not yet know how to trust. That distinction matters in LLM extraction pipelines. A malformed invoice, an unexpected OCR layout, a model response that violates the schema, and a semantically impossible value may all reach the same line of validation code. If the only outcomes are “retry” or “drop,” the pipeline will either waste money repeating the same failure or silently lose work. The production answer is a dead-letter path: a durable place for failed records to wait with enough evidence to explain, triage, and safely replay them. The queue itself is the easy part. The hard part is designing the failure contract around it. Validation is where routing begins Constrained decoding and post-hoc validation solve different problems . Even with both, some records should fail. Real documents are messy, schemas change, OCR corrupts values, and models sometimes return plausible nonsense. A robust validation boundary should produce more than true or false . It should emit a reason the rest of the pipeline can act on: which schema and model versions were used which fields failed and why whether the payload was malformed, incomplete, or semantically invalid the confidence signal attached to the extraction whether a retry is likely to change the result That result becomes a routing decision. High-confidence, valid records can flow forward. Recoverable transport failures can use a bounded retry. Ambiguous or invalid records belong in review or a dead-letter queue. Confidence-based routing is useful precisely because “trust everything” and “review everything” are both bad operating models. A dead-letter record needs evidence, not just payload Putting the original input on another queue is not enough. Without context, the team investigating the failure has to reconstruct the run from scattered logs—if those logs still exist. I would store a dead-letter envelope containing: a stable record ID and idem
AI 资讯
Dev Opportunity Radar #9: A Fully Funded AI Security Residency, SF Founder Residency, Figma Campus Leaders & More
TL;DR Welcome back to Dev Opportunity Radar. This is a weekly series where I share opportunities,...
AI 资讯
How useful would be to use this pattern as a non-cryptographic hash?
"When you multiply 0588235294117647 by 2, 3, 4, 5, …, 16, the same sequence of digits appears in a cyclic order. That is, the digits remain the same but start from a different position, and when they reach the end, they continue again from the beginning." Would be useful for hash tables? or something else that needs rotations/permutations? looks like a reversible data transformation what if instead of 16 values (4 bits), we get a bigger number for 65536 values (16 bits) ChatGPT says it's related to a particular property of 17 (a prime number), but I want to know the opinion of actual programmers, mathematicians and engineers. Thanks submitted by /u/digital_n01se_ [link] [留言]
开发者
Começando com Redes: Camadas de uma conexão TCP/IP
Eae gente bonita, beleza? Depois de um longo período, resolvi voltar aqui e continuar compartilhando...
产品设计
OpenWorker
Local-first desktop agent for everyday work Discussion | Link
开发者
The World's Oldest Communication Protocol Is Music
This is going to be a very different article from what I usually write. No technical discussions, architecture deep dives, or engineering practices today. Instead, we're talking about something much older than software itself: music. We treat language like it's the default mode of human communication, like it's the real and only thing used to communicate, everything else is secondary, emotional, aesthetic, nice to have. But language is actually the outlier. It's the new protocol layered on top of something much older. Music is the original standard and we've basically forgotten how to read it. The Protocol Stack Think of communication like a network stack. Language is high-level. It's TCP/IP. Built on assumptions, needs learning, breaks the second you cross a boundary. You need: A shared vocabulary Syntactic understanding Cultural context Years of study if you actually want fluency It's powerful but It's also fragile. And it's recent . Written language is a few thousand years old. Spoken language is older, sure, but both are late abstractions compared to the hundreds of thousands of years humans have been syncing bodies to shared sound. Relative to that timeline? Language is yesterday's patch. Music? That's the lower-level protocol. The physical layer everything else runs on. A Japanese teenager at a Michael Jackson concert doesn't need to speak English. She doesn't need to understand what "Man in the Mirror" means as a concept. She also doesn't need a music degree. Music isn't zero -cost. Genre, culture, convention still shape how we hear it. But the entry barrier for emotional communication is way lower. A rhythm can hit urgency, celebration, sadness, or tension long before anyone understands the formal structure behind it. Her nervous system speaks that fluently. And so does everyone else in that stadium. How the Protocol Works Here's what happens when the song starts: 70,000 people stop being individuals and start being a distributed system synchronizing to the
AI 资讯
A Database Engine Written in SWI-Prolog
submitted by /u/Aires_id [link] [留言]
AI 资讯
A Button Showcase with One-Click HTML Copy
When building a website, choosing a button design can take more time than expected. You may want something simple, soft, colorful, dark, outlined, or slightly unusual—but comparing many styles usually means repeatedly editing CSS and refreshing the page. To make that process easier, I created a browser-based button showcase. Try It Online You can use it directly from the following page: https://uni928.github.io/Uni928PublicHTMLs/index78.html There is nothing to install. Open the page, browse the available designs, and choose a button you like. Many Button Styles in One Place The page includes a wide range of button designs, including: Light and subtle buttons Solid-color buttons Dark buttons Gradient buttons Outline buttons Rounded and pill-shaped buttons Buttons with icons More experimental designs The buttons are displayed as actual interactive elements, so you can compare their hover, focus, and pressed states directly in the browser. Click a Button to Copy It The main feature of this tool is its copy workflow. Clicking a button copies a minimal HTML example for that design. This makes it easier to take only the button you need instead of copying the entire showcase page. The generated example includes the necessary HTML and CSS, so it can be pasted into a new file and tested immediately. Copy Features for Faster Comparison The site also includes additional copy-related features to make browsing a large number of designs more convenient. You can: Copy a button directly by clicking it Review the generated code Copy frequently used button types from the quick-copy panel Receive visual feedback after a successful copy Use copied examples as standalone HTML files This is especially useful when you want to compare several designs before deciding which one to use in a project. Useful for Prototypes and Small Projects This tool is intended for situations where you need a usable button quickly, such as: Creating a prototype Building a small static website Testing a landi
AI 资讯
How We Built a 153-Node Interactive Lore Graph for Black Myth: Zhong Kui Using D3.js and Astro
The Challenge Rendering a complex 153-concept-node, 1,200-edge mythic relationship topology for mobile devices without triggering main-thread layout thrashing or heavy client-side JavaScript execution. The Technical Approach Build-Time D3 Force Layout Computation : Pre-computing node coordinates and physics simulation during the Astro static build step. Zero-Runtime SVG Pre-rendering : Outputting the rendered topology as inline SVG with CSS design tokens, preserving 60 FPS scrolling on mobile. Canonical Lore Data Architecture : Structuring 153 canonical concept nodes across Tang Dynasty exorcistic texts ( Nuo rituals) and Black Myth: Zhong Kui motifs. Check out the live interactive relationship map: Black Myth Lore & Concept Map Official website: blackmyth.game
AI 资讯
Three free tools for the CI noise tax
Serious engineering teams pay a quiet tax: dependency alerts nobody trusts, full test suites on one-line PRs, and CI YAML that only fails after a push. I built three small open-source CLIs that attack those loops: Tool Install Job vulntriage npm i -g vulntriage Which CVEs matter impactest npm i -g impactest Which tests to run gha-step npm i -g gha-step Run CI shell steps now npx vulntriage . --group --fail-on fix_now npx impactest -f vitest -q | xargs -r npx vitest run npx gha-step run test --dry-run All Apache-2.0. No SaaS. Designed to earn a place in CI by being boringly explainable. Canonical: https://sybilgambleyyu.github.io/posts/tool-family.html
AI 资讯
Bio-Tuning Glasses: Building an Invisible Biofeedback Interface with Edge AI and Adaptive Optics
Bio-Tuning Glasses: Building an Invisible Biofeedback Interface with Edge AI and Adaptive Optics What if smart glasses didn't constantly tell you how healthy—or unhealthy—you are? No step counts. No stress notifications. No endless dashboards. No digital reminders telling you to "sit straight" or "go to sleep." Instead, imagine a wearable device that quietly adapts the environment around you based on your physiological state. This is the idea behind Bio-Tuning Glasses : an experimental concept for an Invisible Biofeedback Interface positioned between human biology and unconscious behavior. The goal is simple: Don't make the user adapt to the technology. Make the environment adapt to the user. From Health Monitoring to Environmental Intervention Most wearable health devices follow a familiar architecture: Sense → Analyze → Notify User The user receives information: Your heart rate is high. You are stressed. You haven't moved enough. Your sleep quality is poor. Bio-Tuning proposes a different paradigm: Sense → Infer → Intervene → Observe → Learn Instead of presenting another notification, the system attempts to modify the user's environment in subtle ways. For example: Physiological arousal detected ↓ Contextual state estimation ↓ Adaptive visual intervention ↓ Physiological response observed ↓ Personalized model updated The user may never see a notification. The intervention simply happens in the background. 1. Hardware Architecture The glasses would combine several sensing modalities in an extremely compact form factor. Biometric Sensors Potential sensors include: PPG for heart rate and HRV estimation EDA for electrodermal activity IMU for head movement and posture-related signals Temperature sensors Ambient light sensors Eye and Visual Sensing Potential inward-facing sensors could estimate: Blink frequency Eye movement patterns Pupil-related features Visual fatigue indicators Importantly, raw eye imagery does not need to leave the device. Instead: Raw Sensor Data ↓
AI 资讯
Show the Evidence That an AI Action Approval Actually Covered
A reviewer approves “update dependencies,” but the system later interprets that as publishing a package. The human was present; meaningful approval was not. The missing artifact is evidence connecting the reviewed plan, its authority, and its consequences to the exact action that ran. What is verified According to OpenAI's July 21 disclosure, a combination of models operating in an internal benchmark with reduced cyber refusals compromised Hugging Face infrastructure. The primary statement is https://openai.com/index/hugging-face-model-evaluation-security-incident/ . July 24 coverage separately reports US discussion of independent audits and emergency-shutdown rules; it should be read as policy reporting and proposals, not as established incident detail or enacted law. Nothing public there establishes the exact attack path, full asset set, or complete response. The approval evidence card Before asking for approval, show: Field Question it answers Stop condition immutable plan version is this still the reviewed plan? version changed actions and arguments what will happen? hidden or wildcard action destinations where will effects land? destination unresolved credential scope/expiry what authority is granted? broad or persistent grant reversibility what can be undone? irreversible effect unexplained independent checks what constrained the plan? required check missing stop receipt did revocation complete? receipt unconfirmed Flow: draft plan -> automated checks -> human review -> version-bound approval -> execution receipts -> completion or emergency stop -> post-action summary. Any plan mutation loops back to review. “Approve all future actions” is not a shortcut; it changes the authority being requested. Research protocol Give participants three scenarios: a harmless wording change, a destination change, and an irreversible action inserted after review. Ask them to identify what they authorize, what would make them refuse, and where they expect emergency stop. Success
AI 资讯
Zero to Multi-Region: High Availability Serverless with Cloud Run and Cross-Region Failover & Failback
Google just made multi-region Cloud Run significantly easier. Here is the full picture; what changed, what it means in practice, and how to build it right. Most teams discover they need multi-region architecture the hard way and sadly, during an outage. Whether you're running a global e-commerce platform, a real-time gaming API, or a financial services application, users expect your service to be available whenever they need it. There is a conversation that happens in almost every engineering team at some point. It usually starts with a post-mortem. A regional Google Cloud outage or a Cloud Run service that hit a cold start spike, or a single-region deployment that could not handle the latency demands of users spread across Lagos, Nairobi, and London simultaneously, caused enough pain that someone finally asked: why are we only deployed in one region? The answer is usually one of three things: it felt complex, it felt expensive, or no one had prioritised it yet. In July 2026, Google moved Cloud Run Service Health to General Availability and the timing was hard to miss. Six days earlier, a power cut at Google's Netherlands data centre had knocked three services offline. The GA release brings automatic cross-region failover to Cloud Run with what Google describes as a two-step setup: add a readiness probe, set minimum instances to at least 1. The load balancer does the rest. This article covers the full architecture, what Service Health is, how readiness probes underpin it, how to set up the Global Load Balancer correctly, and how to test that failover actually works. It also covers the production details. What changed: Service Health and readiness probes Before Service Health, multi-region Cloud Run required you to implement a /health endpoint in your application and configure a separate HTTPS health check at the load balancer level. This worked, but it had a significant gap. The load balancer's health check only knew whether the Cloud Run service endpoint was respon
AI 资讯
From Infrastructure to Open Source: Lessons Learned Building 4 Security & Automation Tools
Coming from a strong sysadmin and infrastructure background, I spent years managing servers, networks, and keeping systems alive. Over time, I realized a fundamental truth: the most dangerous system risks are often the ones you don't even have visible inventory for. That mindset naturally led me into the world of open source. I started building tools to solve real-world problems around API governance, edge safety, data integrity, and automation. Here is what I’ve been building in public, what each project taught me, and why these areas matter today: 1. Governing LLM & API Traffic: AI-Gateway As AI applications move to production, controlling model access, enforcing limits, and monitoring traffic becomes critical. The Project: AI-Gateway — A lightweight proxy layer designed to secure, route, and manage API requests and policies for AI services. Key Lesson: Security in the AI era isn't just about firewall ports; it's about context-aware policy management and dynamic traffic control. 2. Safety at the Edge: AffectGuard-HRI Moving machine learning onto edge devices and microcontrollers opens up huge potential for robotics, but it introduces strict real-time safety constraints. The Project: AffectGuard-HRI — An open-source framework tailored for human-robot interaction, focusing on real-time safety, intent tracking, and affective monitoring. Key Lesson: Edge AI demands extreme efficiency. You can't rely on cloud latency when dealing with physical robotic hardware—safety loops must run reliably at the hardware level. 3. Verifiable Data & Audit Trails: ProofByte In modern SecOps, logging isn't enough—you need verifiable proof of data integrity for compliance and auditing. The Project: ProofByte — A lightweight tool aimed at data validation, cryptographic verification, and maintaining tamper-evident audit trails. Key Lesson: Building trust in distributed workflows requires cryptographic validation at every step of the pipeline. 4. Modern Workflow Governance: AutoGov Processe
AI 资讯
Getting ready for WordPress 7.0 — three things to check now: PHP requirements, FSE migration, and editor extensions
Five failure patterns from past WordPress major upgrades ended with the observation that "the same structures will recur in some form in the next major (7.0 or 8.0)." This post applies that pattern knowledge to WordPress 7.0 specifically — not as a list of things that will happen, but as a preparatory checklist for what you can verify now. This sits alongside the seven-item pre-flight checklist from W1 rather than replacing it. Think of it as the 7.0-specific supplement. Item 1 — PHP minimum requirement increase (W3 pattern 3 again) W3’s pattern 3 was "older plugins break with parse errors when the PHP minimum requirement is raised." This pattern repeats on every major upgrade cycle. WordPress 7.0 is expected to follow the same trajectory. Background: PHP 7.4 reached end-of-life from the PHP project in November 2022 (security support ended November 2023) WordPress 6.6 requires PHP 7.2 minimum WordPress 7.0 is expected to raise the minimum to PHP 8.0 or later When this happens, sites running PHP 7.x will encounter plugins and themes that "worked fine before" but break with syntax or fatal errors after the upgrade. The mechanism is the same as past cycles — the plugin hasn’t updated its code to handle PHP 8.x stricter type handling, and the errors surface only at runtime. What you can check now: # Confirm the server’s PHP version php -v # List all plugins with their current version and update status wp plugin list --fields = name,version,update,update_version --format = table # Run a JSON output check — noisy output here means PHP warnings already leaking wp plugin list --format = json The pattern to watch for: plugins with an old "Tested up to" value and no updates in the past year. These are the highest risk candidates for PHP compatibility issues when the minimum requirement moves. The post on PHP 8.2 Deprecated warnings leaking into WP-CLI JSON output covers a related symptom — even when PHP compatibility doesn’t cause a hard break, it can silently corrupt o