Stop Fearing the Blinking Cursor: Overcoming Terminal Anxiety

The first time I opened a terminal, I stared at it for a solid minute.

No buttons. No menus. No helpful icons.

Just a black screen and a blinking cursor that seemed to be asking, “Well? What are you waiting for?”

If you’ve ever felt intimidated by the command line, you’re not alone. The terminal has an unfair reputation. Movies portray it as a mysterious tool used by elite hackers, while online forums are full of stories about people accidentally deleting important files with a single command.

It’s enough to make any beginner nervous.

The most common fear is simple:

“What if I type the wrong thing and break my computer?”

The good news is that the terminal is far less dangerous than most people think. In reality, it’s not a weapon waiting to be misused—it’s a conversation between you and your operating system.

Like any conversation, once you learn a few basic words, everything starts to make sense.

In this article, we’ll strip away the mystery and focus on a handful of beginner-friendly commands that help you navigate your system confidently. By the end, you’ll understand how to move around, find your way back when you’re lost, and—most importantly—stop being afraid of that blinking cursor.

The Myth of the “Self-Destruct Button”

The Fear

“I’m going to destroy my computer by typing the wrong thing.”

It’s a reasonable concern. The terminal looks powerful because it is powerful.

The Reality

Your operating system has safeguards in place. Most potentially dangerous actions require elevated privileges, often through commands such as sudo, which stands for SuperUser Do.

Without those permissions, your computer actively prevents many system-level changes.

For beginners learning navigation commands, you’re essentially exploring in a safe environment. You’re looking around, not rewiring the house.

Think of the Terminal as Read-Only Exploration

Before changing anything, let’s learn how to look around.

These commands are completely safe because they only display information.

pwd — Where Am I?

Think of pwd as your GPS.

pwd

Output:

/home/brendan/projects

It simply tells you your current location in the filesystem.

No files are modified.
Nothing is deleted.
Zero risk.

ls — What’s Here?

Think of ls as opening your eyes.

ls

Output:

Documents Downloads Pictures projects

It lists the contents of your current directory.

Again, nothing changes. You’re simply observing.

When I was learning the terminal, I probably ran ls hundreds of times because I was constantly checking where I was and what was around me.

That’s completely normal.

Taking Control: Moving Around

Once you know where you are, it’s time to move.

cd — Change Directory

Imagine clicking through several folders in a graphical file manager:

Documents → Projects → Portfolio → Website

In the terminal, you can jump there instantly:

cd Documents/Projects/Portfolio/Website

No clicking required.

The more projects you work on, the more you’ll appreciate how much faster this becomes.

cd .. — The Universal Undo

Made a wrong turn?

Use:

cd ..

This moves you back one directory level.

Think of it as the terminal equivalent of saying:

“Actually, take me back.”

It’s one of the most useful commands you’ll ever learn.

What to Do When You Get Stuck

Every beginner eventually runs into a moment where:

  • A command won’t stop running.
  • The screen fills with text.
  • Something looks confusing.
  • Panic starts creeping in.

Fortunately, the terminal comes with built-in escape hatches.

Ctrl + C — Emergency Stop

If a command seems stuck, press:

Ctrl + C

This tells the terminal:

Stop whatever you’re doing and give me my cursor back.

Learning this shortcut instantly made me more confident because I realized I wasn’t trapped if I made a mistake.

clear (or Ctrl + L) — Start Fresh

When your screen becomes cluttered:

clear

Or use:

Ctrl + L

The terminal clears the screen and gives you a clean workspace.

Sometimes the best debugging technique is simply removing the visual chaos and starting again.

Final Thoughts

The terminal isn’t scary because it’s dangerous.

It’s scary because it’s unfamiliar.

Every developer, system administrator, DevOps engineer, and cybersecurity professional started exactly where you are now: staring at a blinking cursor and wondering what to type next.

The secret isn’t knowing hundreds of commands.

It’s knowing a few basic ones well enough to explore confidently.

Start with:

  • pwd
  • ls
  • cd
  • cd ..
  • clear

And remember your panic button:

  • Ctrl + C

Once you master these, that blinking cursor stops looking intimidating and starts looking like an invitation.

Do you remember your first experience with the command line? What surprised you the most?

The Developer Job Description Quietly Rewrote Itself in 2026. Did You Notice?

Here’s a number that will either reassure you or unsettle you depending on where you sit in the developer hierarchy.

Workers with AI skills earned 56% more than colleagues doing the same jobs without those skills in 2026. The year before, that premium was 25%. The year before that, 18%. It’s not flattening. It’s accelerating.

Here’s the other number: senior software developers saw a 10% salary decline year-over-year. Senior. Not junior. Not mid-level. Senior developers with years of experience watched their market rate fall while a subset of their peers with specific AI fluency saw theirs climb at 9.2% annually.

The job didn’t collapse. It forked.

One fork leads toward a market where your value is measured by how much code you produce, and AI has made that kind of value dramatically cheaper. The other fork leads toward a market where your value is measured by whether you can design systems, direct agents, govern output quality, and make architecture decisions that AI can’t make on its own.

The developers choosing the second fork — deliberately, not accidentally — are the ones whose compensation trajectories look anomalous in a flat market. This blog is about how they made that choice and what it actually looks like in practice.

What happened to the job description

The traditional senior developer job description, simplified, looked like this: deep expertise in a specific language or framework, strong ability to write correct and maintainable code, ability to debug complex problems, ability to mentor juniors and review PRs.

The 2026 version of that job description at a well-run engineering organisation looks notably different. The emphasis has shifted from writing to directing, from syntax to architecture, from individual contributor to systems thinker.

Addy Osmani, engineering leader at Google, described the shift as moving from coder to conductor to orchestrator. At the start of 2024, AI-assisted programming resembled a significantly improved autocomplete. By 2026, the transition is to agent systems that operate on codebases over time.

What that means concretely: the most valuable thing a senior engineer can do in 2026 is not write a feature. It’s define the architecture that lets an agent write the feature correctly. It’s create the context files that make agents generate consistent, idiomatic code. It’s build the test infrastructure that catches what agents get wrong. It’s review the agent’s output with the judgment of someone who understands the system deeply enough to spot the subtle errors.

The skills that mattered most in 2021 — fast accurate implementation, deep framework knowledge, encyclopaedic API recall — are the exact skills that AI tools have made cheaper. The skills that matter most in 2026 are the ones AI tools don’t have: judgment, architectural thinking, understanding of tradeoffs, and the ability to know when AI output is wrong even when it looks right.

The two roles senior developers are splitting into

Senior developers in 2026 are effectively splitting into two categories: code validators and architects. If you choose validation — code review — you’ll be in demand but burned out.

That framing is a little stark, but it captures something real. Here’s a fuller picture of what each path looks like.

Path 1: The AI Orchestrator

This is the role that has the strongest compensation trajectory and the most clearly defined skill set.

An AI orchestrator doesn’t primarily write code. They design the system within which agents write code. They author the context files, rules, and architectural boundaries that guide agent behaviour. They define acceptance criteria that agents must satisfy. They review agent output for architectural soundness, not just syntactic correctness. They manage the MCP integrations that give agents access to the right tools. They run parallel background agents on different parts of the codebase and integrate the outputs.

The skills this requires are old and new simultaneously. Old: system design, domain modelling, architectural pattern knowledge, understanding of non-functional requirements (performance, security, scalability). These are the skills that take years to develop and can’t be replaced by prompt engineering. New: context engineering, agentic workflow design, knowing which tasks to delegate and how to brief them, governing AI output quality at scale.

The new standard for senior developers is context orchestration — the ability to guide agentic IDEs that understand the entire repository, documentation, and architectural patterns.

Path 2: The Specialist Who Goes Deeper

Not every developer needs to become an AI orchestrator. The other path with a strong compensation trajectory is going deep in areas where human expertise is still clearly irreplaceable and where AI tools augment rather than replace.

Security engineering is one example. The tools generate insecure code at a documented rate and are worst at the subtle architectural vulnerabilities that only deep security knowledge catches. Security engineers who understand the full threat model, can trace attack surfaces through AI-generated code, and can design systems that are secure by construction are in shorter supply in 2026 than they were in 2024, not longer. The AI made the problem bigger, not the expertise less valuable.

Performance engineering is another. AI generates functionally correct code. It doesn’t generate code that’s been profiled against your actual traffic patterns, optimised for your specific hardware characteristics, or designed with the full operational context in mind. The developer who can take AI-generated output and make it production-ready from a performance standpoint is doing work AI doesn’t do.

Distributed systems architecture is a third. The agent can implement a microservice. It can’t decide whether you should have a microservice, whether the boundaries are right, whether the consistency model is appropriate, or whether the failure modes are acceptable for your specific reliability requirements. Those decisions require human judgment informed by years of building things that broke in unexpected ways.

GenAI Engineer and MLOps Specialist postings are growing at 2–3× the rate of traditional roles year-over-year. Developers who layer AI fluency on top of solid software fundamentals are commanding salary premiums of 15–25% above peers without AI skills.

The salary data, stated plainly

AI Engineers earn 12% more than general Software Engineers in equivalent roles and levels. The premium is higher for specialised LLM developers, who average $209,000. Mid-level AI engineers saw the highest year-over-year gains at 9.2%. Senior software developers without AI specialisation saw a 10% salary decline year-over-year.

Workers with AI skills earn 56% more than same-role colleagues without those skills — a premium that has more than doubled from 25% in 2024. The premium accrues almost entirely to experienced practitioners with genuine production depth. Surface familiarity with AI tools is now table stakes, not a differentiator.

That last sentence matters more than the headline number. The 56% premium doesn’t accrue to developers who have used Copilot for six months. It accrues to developers who have built production AI systems, who understand the failure modes, who have fixed things when they broke, and who can make architectural decisions about where AI belongs in a system and where it doesn’t.

The premium is accessible. It’s not easy.

What “AI fluency” actually means for career development

There’s a version of this conversation that reduces to “learn prompt engineering.” That’s not what the salary data is tracking. Here’s what it’s actually tracking.

System-level thinking about AI integration. Understanding where AI agents belong in a technical architecture — what they should be trusted to do autonomously, what should require human review, what should never be delegated. This requires understanding the failure modes well enough to design around them. Developers who have shipped things that broke because of AI output and understand exactly why are more valuable than developers who have only shipped things that worked.

Context engineering as a discipline. The ability to create the information environment — CLAUDE.md files, rules files, architecture decision records, system prompts — that makes agents perform reliably and consistently. This is not writing prompts. It’s designing the persistent layer of institutional knowledge that makes AI tools behave like they understand your system.

Quality governance at scale. As AI generates more code faster, the bottleneck shifts to quality validation. The ability to review AI output efficiently, catch what’s subtly wrong, and build automated validation that scales — test suites, linters, security scans, architecture fitness functions — is increasingly what distinguishes teams that ship reliably from teams that ship fast and break things.

MCP and tool integration design. Deciding which external tools agents should have access to, what permissions are appropriate, how to scope access safely, and how to design workflows that use agents effectively for real tasks. This is infrastructure thinking applied to AI tooling.

None of these skills are learned by watching tutorials. They’re learned by building things, having them break, understanding why, and building better the second time. The developers who are making the transition well are the ones who have enough production experience that their AI fluency is anchored in real understanding — not just fluency with the tools themselves.

The honest roadmap for developers who want to make this shift

If you’re a developer who has been coding for three-plus years and wants to shift toward the higher-value end of this market, here’s what that actually looks like as a practice.

Build and break a production AI system. Not a toy project. Something with real traffic, real data, and real failure modes. The goal is to experience what happens when an AI component behaves unexpectedly in production — hallucination, context drift, reasoning errors under edge cases — and to understand how to build systems that handle those failures gracefully. This experience is what the premium is actually paying for.

Get deep on context engineering. Spend a week doing nothing but improving your CLAUDE.md and rules files and measuring the output quality difference. Build architecture decision records for your team’s non-obvious conventions. Create a Codebase Orientation document for the project you know best. This practice builds the intuition for what makes AI tools effective at the system level.

Take ownership of your team’s AI governance. If your team is using AI tools without systematic quality governance, offer to build it — the code review standards for AI-generated code, the security review checklist, the test coverage requirements. This work is undervalued, genuinely useful, and directly demonstrates the skills the market is paying for.

Write the architectural documentation that AI tools need. Go through your codebase and document the decisions that AI tools consistently get wrong because they don’t have the context. Why does the payments module use optimistic locking? Why is the notification system pull-based rather than push-based? Why does the user authentication flow have that seemingly redundant step? This documentation is valuable for the AI tools that will read it, valuable for the humans who will onboard to this codebase, and it demonstrates exactly the kind of architectural understanding that commands premium compensation.

Pick one deep specialisation and go all the way in. The developers with the strongest compensation trajectories in 2026 are not generalists with light AI fluency. They’re specialists — security engineers, performance engineers, distributed systems architects — with deep expertise in areas where AI tools create new problems rather than solving them, augmented by AI tool fluency that makes them dramatically more productive. Pick the specialisation that matches your existing depth and go deeper, not sideways.

What the next two years look like

The transition from coder to orchestrator is already taking place and is expected to be mostly complete by 2028. The transition represents a fundamental shift in how software is created — humans will spend less time writing code and more time directing AI agents to do so.

The framing of “will AI take developer jobs” is the wrong question. The right question is: which developer skills will be more valuable in two years and which will be less, and how do you make sure you’re developing the right ones?

The skills that will be less valuable: fast accurate implementation of known patterns, deep memorisation of framework APIs, the ability to write boilerplate quickly. AI has made all of these abundant.

The skills that will be more valuable: system architecture, security judgment, performance engineering, the ability to specify precisely what you want and verify that you got it, understanding of AI failure modes, and the accumulated intuition that comes from building production systems and watching them break.

AI didn’t change what matters. It just made the shortcuts disappear. The developers who thrived by “knowing the frameworks” are now struggling. The ones who understood why systems work the way they do are thriving.

The market has not been subtle about this. The compensation data is clear and the direction of the premium is consistent. The developers who are positioned well are the ones who treated the last two years as an opportunity to go deeper — not the ones who used AI tools as a reason not to.

Originally published on ZyVOP

Your NAS Is Loud Because of Docker (and How to Fix It)

You buy a NAS for silent, always-on storage. It sits in a corner, humming quietly, doing its thing.

You installed Docker on it for the same reason I did: to save money. Every open-source service you’d otherwise pay a VPS for — your media server, your download automation, your file sync, your home automation bridge — all of it can run on the NAS for free. No monthly VPS bills, no cloud subscriptions, no $5/mo here and $10/mo there that add up to a second rent. Just one box, your box, doing everything.

The problem is that Docker wasn’t designed for spinning disks.

And suddenly the HDDs never stop. Seeking, spinning, clicking, whirring — not occasionally, not every few minutes, but constantly. At 2am you can hear it from the next room. Through a closed door. It drives you insane because you bought this thing specifically so it would not make noise.

I know, because I lived with it for months. Every night, the same clicking. Every morning, the same relief when the TV drowned it out. The NAS was supposed to be invisible, and instead it was the loudest thing in the house.

Here’s what causes it, and how I went from that to 99.9% less noise in one afternoon.

What’s Actually Causing the Noise

Mechanical HDDs make noise when the read/write head moves. The more random the I/O — small reads and writes scattered across the disk — the more seeking, the more noise. Sequential writes to a single file are quiet. Random I/O across thousands of small files is loud.

Docker is pathological for HDDs.

Docker overlay2

Docker’s default storage driver is overlay2. Every container runs on top of layered filesystems — the image layers are stacked, and a thin writable layer sits on top for each running container.

Every file operation inside a container that touches a file from a lower layer triggers a copy-on-write: the entire file gets copied up to the writable layer before the write happens. On an SSD this is fast and silent. On spinning HDDs with mechanical heads, every copy-on-write is a seek, a read, and a write — often scattered across the disk.

And it’s not just copy-on-write. Docker’s overlay2 metadata lives in small files across a deep directory tree. Container startup reads dozens of these. Log rotation writes to them. Health checks touch them. Any container doing anything at all generates constant scattered I/O.

Now multiply that by however many containers you’re running. Every single one is generating random I/O all day, every day. The HDDs never get a break.

Everything Else Piling On

Beyond Docker, a typical homelab NAS has:

  • System monitoring tools running on cron (every 5-10 minutes, writing stats to disk)
  • systemd journal flushing logs
  • The NAS OS itself doing housekeeping

None of these alone would be noticeable. But on top of Docker’s constant churn, the drives never spin down. Not for a second.

Diagnosing Which Process Is the Problem

Before moving anything, confirm what’s actually hitting the disk:

# Real-time I/O per process (needs sysstat)
sudo iotop -o

# Disk utilization over time
iostat -x 2 10

On a typical Docker setup you’ll see the Docker daemon at the top, with periodic spikes from cron jobs.

The Fix: Move Docker Off the HDDs

The HDDs are loud because they’re doing work they shouldn’t be doing. The solution is to give that work to something that doesn’t make noise.

An external SSD connected via USB is cheap, silent, and fast enough for everything Docker needs. USB 3.0 to a SATA SSD delivers 400+ MB/s — far more than any container workload requires.

The goal: the HDDs only handle the NAS OS and your actual data (media, documents, backups). Everything Docker-related moves to the SSD.

What to Migrate

Docker data-root — the overlay2 layers, image cache, container writable layers. This is the biggest source of random I/O and the highest-impact thing to move.

# /etc/docker/daemon.json
{"data-root": "/mnt/external-ssd/@docker"}

Bind-mount volumes — the persistent data your containers read and write (databases, config files). If these live on the HDD, container writes hit the HDD. Move them to the SSD.

For bind mounts, a symlink keeps things transparent — containers keep using the same paths, no reconfiguration needed:

ln -s /mnt/external-ssd/volumes /original/volumes/path

What Stays on the HDDs

  • The NAS operating system
  • System logs
  • Your actual data files (documents, media, backups) — these have sequential I/O patterns that HDDs handle well and don’t cause the constant seeking noise

A Note on Copying Files (UGOS Pro Caveat)

If your NAS runs UGOS Pro (UGREEN’s Debian-based OS), there’s a critical gotcha: rsync will silently corrupt permissions when copying from the NAS filesystem to an external drive. This is caused by proprietary kernel-level xattr hooks in UGOS Pro.

The fix and the full technical explanation are in a separate post: Why rsync Destroys Permissions on UGOS Pro — and the Only Fix That Works

Short version: use tar --xattrs-exclude='ug.*' instead of rsync for any file copy on UGOS Pro.

Mount the SSD Correctly

# /etc/fstab
UUID=<your-uuid> /mnt/external-ssd ext4 defaults,noatime,nofail 0 2

Two flags matter:

  • noatime — disables access time updates on every file read. Eliminates a whole class of unnecessary writes.
  • nofail — if the SSD disconnects and the NAS reboots, it boots normally instead of hanging at the fstab error.

The Result: Silence

After the migration, the difference is night and day. Before, the HDDs were seeking constantly — a low but relentless clicking that never stopped, day or night. It was the kind of noise you don’t notice during the day but drives you crazy at 3am when the house is silent.

Now? Nothing. The HDDs spin up occasionally — a system log flush, a cron job writing stats — but the constant background noise is gone. The drives spend most of their time parked, doing what they were designed to do: sit there quietly and hold your data.

I’d estimate 99.9% reduction in audible HDD activity. The clicking that used to drive me insane? Completely gone. The NAS is back to being the quiet box in the corner it was always supposed to be.

You don’t realize how much that noise was bothering you until it stops. Trust me.

If your NAS runs UGOS Pro, read the companion post before attempting the migration — the rsync issue will cost you time if you hit it blind: Why rsync Destroys Permissions on UGOS Pro

Stop Pasting Tokens: OAuth2 Login for JetBrains IDE Plugins

The moment a plugin needs account data, a simple API call turns into an authentication problem. The bad shortcut is familiar: ask the user to create a personal access token (PAT), make them paste it into settings, and hope it never leaks.

For a JetBrains IDE plugin, use this flow instead: the user clicks the Login button, the browser opens, the provider handles sign-in, the IDE receives a callback, and the plugin stores the token.

At a high level, the plugin will:

  1. Open the provider’s authorization page in the browser.
  2. Receive the OAuth2 callback inside the IDE.
  3. Validate the returned state.
  4. Exchange the authorization code with PKCE.
  5. Store the access token in PasswordSafe.

This post uses GitHub as the OAuth2 provider, but the same shape works elsewhere. Scopes, URLs, token responses, and refresh rules will change.

Sample code: https://github.com/JetBrains/intellij-sdk-docs/tree/main/code_samples/oauth2

The Mental Model

OAuth2 is easier to reason about as hotel key cards.
At check-in, you do not get a master key. You get a card for your room, maybe the elevator or gym. When your stay ends, the card stops working.

That is the useful bit: allowed access, but limited and temporary. An OAuth2 access token works the same way. The user signs in with the provider, and the plugin gets a token for the API access the user approved. The plugin never needs the user’s password.

That approach is better than asking people to paste a long-lived secret into settings. Users stay in the browser login flow they already trust, while the provider keeps control of scopes, expiration, and revocation.

So the goal is simple: get the plugin a limited token without making the user paste one manually. The catch is that a desktop plugin cannot protect a traditional client secret.

Why PKCE Is Part of the Story

In a web app, the server can keep a client secret on the backend. A desktop plugin cannot do that. Anything bundled into the plugin can be inspected.

That is where PKCE comes in. PKCE stands for Proof Key for Code Exchange, and it ties the returned authorization code to the login request that created it.

Before opening the browser, the plugin creates a random code_verifier and sends GitHub a derived code_challenge. Later, when GitHub redirects back with a temporary code, the plugin sends the original verifier to the token endpoint.

GitHub compares the verifier with the earlier challenge. If they do not match, no token. That means the returned code is not enough on its own, which is exactly what we want for a desktop plugin.

The Flow

Here is the full flow:

  1. The user clicks Login with GitHub.
  2. The plugin creates state, code_verifier, and code_challenge.
  3. The plugin opens GitHub’s authorization URL in the browser.
  4. GitHub redirects back to the IDE with state and a temporary code.
  5. The plugin validates state.
  6. The plugin exchanges the code and verifier for an access token.
  7. The plugin stores the token in PasswordSafe and calls the GitHub API.

Where the Flow Lives in Code

The sample code lives in code_samples/oauth2. The flow above is split across four small pieces:

  • plugin.xml registers the settings UI and the local callback handler.
  • AuthConfigurable gives the user the login and logout buttons.
  • AuthRestService handles the request that GitHub sends back to the IDE’s built-in HTTP server.
  • AuthService creates the OAuth2 request, exchanges the code, stores the token, and calls the API.

That split is the main thing to notice. OAuth2 feels messy when everything is described as one big mechanism. In code, it is much easier to follow when each class owns one part of the trip.

Register the UI and Callback

The plugin descriptor registers two things:

  • the settings page
  • the local HTTP callback handler
<extensions defaultExtensionNs="com.intellij">
  <applicationConfigurable
      instance="org.intellij.sdk.oauth2.AuthConfigurable"
      id="org.intellij.sdk.oauth2.AuthConfigurable"
      displayName="My Plugin Auth"/>

  <httpRequestHandler implementation="org.intellij.sdk.oauth2.AuthRestService"/>
</extensions>

applicationConfigurable adds the settings page. httpRequestHandler registers a handler with the IDE’s built-in HTTP server, so a request to /api/myplugin can be routed to AuthRestService. That gives GitHub a local redirect target after browser authorization.

Keep the Settings UI Boring

AuthConfigurable is the settings UI. In the sample, it extends BoundConfigurable, uses the Kotlin UI DSL, and its job is small:

  • if disconnected, show Login with GitHub
  • if connected, show the username and Logout

The panel observes AuthService.state, and the view is a small state switch:

private fun createView(state: AuthState) = panel {
  when (state) {
    is AuthState.Connected -> row("Username") {
      label(state.username ?: "Unknown")
      button("Logout") { authService.logout() }
    }

    is AuthState.Disconnected -> row {
      button("Login with GitHub") { authService.login() }
    }
  }
}

Receive the Browser Redirect

After approval, GitHub redirects back to the IDE’s built-in HTTP server. The callback is handled with the IntelliJ Platform RestService:

http://localhost:<built-in-server-port>/api/myplugin

AuthRestService reads state and code, finds the pending login request, completes it, and returns a small HTML response:

val parameters = urlDecoder.parameters()
val state = parameters["state"]?.firstOrNull()
    ?: return "No authorization state found"
val code = parameters["code"]?.firstOrNull()
    ?: return "No authorization code found"
val callback = service<AuthService>().callbacks.remove(state)
    ?: return "No active OAuth request found"

callback.complete(code)
sendResponse(
  request,
  context,
  response("text/html", Unpooled.wrappedBuffer(HTML_RESPONSE.toByteArray()))
)
return null

After that, AuthService continues the flow by exchanging the code for a token.

Run the Flow

AuthService creates the login request, waits for the callback, and exchanges the returned code:

private suspend fun requestToken(): String {
  val state = UUID.randomUUID().toString()
  val codeVerifier = UUID.randomUUID().toString().padStart(43, '0')
  val callback = CompletableDeferred<String>().also { callbacks[state] = it }

  try {
    BrowserUtil.browse(authorizationUrl(state, codeVerifier))
    return exchangeCodeForToken(callback.await(), codeVerifier)
  } finally {
    callbacks.remove(state)?.cancel()
  }
}

CompletableDeferred is the bridge between the HTTP callback and the coroutine waiting in requestToken(). requestToken() waits on callback.await(), and AuthRestService completes that same object when GitHub redirects back with the code.

The padStart(43, '0') is there because GitHub expects the PKCE verifier to meet the minimum verifier length. Some providers are less strict and may accept a UUID as-is, but GitHub needs the verifier to be at least 43 characters long.

The authorization URL carries both safety checks: state and the PKCE challenge.

private fun authorizationUrl(state: String, codeVerifier: String) = url(
  AUTHORIZATION_URL,
  "client_id" to CLIENT_ID,
  "scope" to SCOPES,
  "state" to state,
  "redirect_uri" to redirectUri,
  "code_challenge" to codeChallenge(codeVerifier),
  "code_challenge_method" to "S256",
)

The challenge is derived from the code verifier:

private fun codeChallenge(codeVerifier: String) =
  DigestUtil.sha256().digest(codeVerifier.toByteArray())
    .let { Base64.getUrlEncoder().withoutPadding().encodeToString(it) }

The actual token exchange is a POST to GitHub’s token endpoint:

private suspend fun exchangeCodeForToken(code: String, codeVerifier: String) =
  withContext(Dispatchers.IO) {
    parseAccessToken(post(tokenUrl(code, codeVerifier), null).readString())
  }

The token request sends back the temporary code and the original verifier:

private fun tokenUrl(code: String, codeVerifier: String) = url(
  ACCESS_TOKEN_URL,
  "client_id" to CLIENT_ID,
  "client_secret" to CLIENT_SECRET,
  "code" to code,
  "redirect_uri" to redirectUri,
  "code_verifier" to codeVerifier,
)

The sample includes a GitHub client secret because GitHub’s OAuth app flow expects one. For a desktop plugin, do not treat that value as secret. PKCE is the useful check here: the returned code is useless without the original verifier.

Store the Token in PasswordSafe

Once the provider returns an access token, store it in PasswordSafe. Regular persistent settings are fine for preferences, but not for access tokens.

The sample uses one credential key:

private val credentials =
  CredentialAttributes(generateServiceName("MyPluginAuth", "OAuthToken"))

On startup, the service restores an existing token if one was saved earlier:

init {
  coroutineScope.launch {
    val token = PasswordSafe.instance.getPassword(credentials) ?: return@launch
    _state.value = AuthState.Connected(fetchUserProfile(token))
  }
}

Storing and clearing go through the same helper:

private fun storeToken(token: String?) =
  PasswordSafe.instance.setPassword(credentials, token)

For a real plugin, use a stable service name. If you support multiple accounts, store one credential per provider account.

Platform sources: PasswordSafe, CredentialStore, and CredentialAttributes.

Calling the API

After login, the rest of the plugin should not care how OAuth2 worked. The sample uses the external org.kohsuke:github-api library and passes the token into GitHubBuilder to fetch the current GitHub username:

private suspend fun fetchUserProfile(token: String): String? =
  withContext(Dispatchers.IO) {
    runCatching { GitHubBuilder().withOAuthToken(token).build().myself.login }
      .onFailure { thisLogger().warn("Failed to fetch user profile", it) }
      .getOrNull()
  }

Keep that boundary in larger plugins too. API code should not know how browser login works.

Wrapping Up

OAuth2 in a plugin is mostly about putting the responsibilities in the right place.

Let the provider handle sign-in. Let the browser handle the user-facing login. Let the IDE receive the callback. Let AuthService deal with the token. And once the token is stored in PasswordSafe, the rest of your plugin can stop caring how the user authenticated.

If you are building something similar, or if you hit an edge case with a provider, bring it to the JetBrains Platform forum.
Good luck!

Bruno: The Git-Friendly API Client and How to Push Your Collection to Codeberg

In the crowded landscape of API development tools, Bruno has emerged as a refreshing, developer-centric alternative to traditional clients like Postman or Insomnia. If you’re tired of bloated interfaces, cloud-only dependencies, and subscription paywalls, Bruno is designed specifically for you.

What is Bruno?

At its core, Bruno is an open-source, offline-first API client. Unlike many competitors that store your collection data in proprietary clouds or require accounts just to test endpoints, Bruno operates entirely on your local machine.

It stores API collections as a folder of standard text files (.bru files) directly in your project’s file system. This design means your API documentation and test collections live alongside your code, not in a siloed ecosystem.

Why Choose Bruno?

  • Offline-first privacy: Runs locally, giving you full control with no forced sync or accounts.
  • Git-friendly collaboration: Track changes, review pull requests, and view diffs with Git.
  • Minimalist performance: Fast, clean interface without heavy browser overhead.
  • Open format freedom: Human-readable .bru files make migration and parsing simple.

Step-by-Step: Push Your Bruno Collection to Codeberg

Because Bruno stores collections as files, versioning them with Git and hosting on Codeberg is straightforward.

Step 1: Create and Locate Your Collection

  1. Open Bruno → Create Collection.
  2. Choose a name and folder location.
  3. To find an existing collection, right-click → Reveal in Finder (macOS) or Open in File Manager (Linux/Windows).

Step 2: Configure .gitignore

Before initializing Git, exclude sensitive data.

cd /path/to/your/bruno-collection
touch .gitignore

Add:

# Ignore Bruno local environment overrides and secrets
environments/*.bru

# Ignore local preferences and cache
.bruno/

Step 3: Initialize Git and Commit Locally

git init
git add .
git commit -m "feat: initial bruno collection setup"

Step 4: Create an Empty Repository on Codeberg

  1. Log into Codeberg.
  2. Click + → New Repository.
  3. Name it (match your collection ideally).
  4. Leave “Initialize Repository” unchecked.
  5. Click Create Repository.

Step 5: Link and Push to Codeberg

git remote add origin https://codeberg.org/your-username/your-repo-name.git
git branch -M main
git push -u origin main

Best Practices for Bruno & Git

  • Keep secrets out: Never push .bru files with production credentials.
  • Commit frequently: Update API docs alongside development.
  • Use pull requests: Handle reviews and merges with Git workflows.

Building a Decentralized Content Pipeline with Nostr & Astro

I recently deployed my fully decentralized content pipeline to kheai.com (you can check out the source code at github.com/kafechew/nostr-seo). After rigorously testing the architecture, I wanted to document exactly how this system works. We are not just building a blog; we are engineering an evergreen, censorship-resistant Web3-to-Web2 bridge.

GitHub logo

kafechew
/
nostr-seo

⚡ Nostr-SEO: The Decentralized Content Bridge

Live Demo
Built with Astro
Powered by Nostr

Nostr-SEO is a high-performance, SEO-first bridge that transforms decentralized Nostr Long-form content (Kind 30023) into a lightning-fast, search-engine-optimized website.

Built on top of the refined Astro-Paper template, this project allows creators to use Nostr as a decentralized CMS while maintaining total ownership of their web presence and search engine rankings.

📖 The Problem & Solution

The Problem

Decentralized protocols like Nostr are amazing for censorship resistance, but their content is often “invisible” to traditional search engines. If you publish only on relays, you lose the benefits of SEO (Search Engine Optimization) and AEO (Answer Engine Optimization) for AI agents like Perplexity and ChatGPT.

The Solution

nostr-seo acts as a Static Site Generator (SSG) for the Nostr protocol.

  1. Fetch: It pulls your articles from decentralized relays at build-time.
  2. Process: It cleans the Markdown, renders complex Math (LaTeX) and Diagrams (Mermaid).
  3. Deploy: It…
View on GitHub

In this tutorial, you will build an automated pipeline that fetches your long-form articles from decentralized Nostr clients (like Yakihonne or Primal), converts them into a blazing-fast static website using Astro, and hosts it for free on Vercel.

Zero prior Astro experience is required. If you follow these steps, you will go from an empty folder to a live, SEO-optimized prototype.

The Architecture Vision

Before writing code, you must understand the data flow. We are shifting from a “push” model (saving files to a server) to a “pull” model (compiling decentralized data at build time).

graph TD
    A[Author on Yakihonne/Primal] -->|Publishes Kind 30023| B(Nostr Relays)
    C[GitHub Actions Cron] -->|Triggers Build every 6h| D(Vercel Edge Network)
    D -->|Fetches Data| B
    D -->|Compiles HTML/CSS| E[Your Live Website]
    E --> F[Human Readers]
    E --> G[AI Agents via llm.txt]

The Tech Stack

Component Tool Purpose
Network Nostr Protocol Decentralized content storage and syndication.
Framework Astro (AstroPaper) High-speed static site generation and routing.
Processor Unified Ecosystem Parsing Markdown, KaTeX (math), and Mermaid (diagrams).
Hosting Vercel Edge delivery and automated webhooks.

Phase 0: The Digital Workbench

You need a few standard tools installed before we begin.

  1. Download and install the LTS (Long Term Support) version of Node.js from nodejs.org.
  2. Install Git from git-scm.com.
  3. Download Visual Studio Code, the industry-standard code editor.
  4. Create free accounts on GitHub and Vercel.
  5. Convert your Nostr npub to a Hexadecimal key using a tool like damus.io/key. Keep this Hex key safe; our code needs it to identify you on the network.

Phase 1: Local Setup & Installation

Open your terminal. We will use the AstroPaper template as our foundation.

Run the following command to download the template into a new folder named nostr-seo:

npm create astro@latest -- --template satnaing/astro-paper nostr-seo

Navigate into your new project:

cd nostr-seo

Now, we must install our communication tools (NDK for Nostr) and our processing engines (Unified for Markdown/Math). Run this command exactly as written:

npm install @nostr-dev-kit/ndk ws nostr-tools remark-math rehype-katex rehype-stringify remark-rehype remark-parse unified --legacy-peer-deps

Note: The --legacy-peer-deps flag prevents npm from crashing if the Astro template’s default formatting tools have strict version conflicts with our new parser.

The MacOS/Linux WebAssembly Patch

If you are on an older OS, the build engine (esbuild) might fail. Open your package.json file and append these properties to force Node.js to bypass your core OS framework and use WebAssembly instead:

{
  "devDependencies": {
    "esbuild-wasm": "0.28.0"
  },
  "overrides": {
    "esbuild": "npm:esbuild-wasm@0.28.0"
  }
}

Open the project in your editor by typing code . in your terminal.

Phase 2: Core Engineering & Data Pipelines

Because we are fetching posts from Nostr relays rather than local .md files, we must create a custom processing pipeline to handle rich text, math equations, and metadata.

Step 1: The Markdown Processor

Create a new file at src/utils/parseMarkdown.ts. This utility converts raw Nostr strings into safe, styled HTML.

import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkMath from "remark-math";
import remarkRehype from "remark-rehype";
import rehypeKatex from "rehype-katex";
import rehypeStringify from "rehype-stringify";

export async function parseNostrMarkdown(content: string) {
  const result = await unified()
    .use(remarkParse)
    .use(remarkMath)
    .use(remarkRehype)
    .use(rehypeKatex)
    .use(rehypeStringify)
    .process(content);

  return result.toString();
}

Step 2: The Event Formatter

Create a new file at src/utils/formatNostrEvent.ts. This extracts SEO descriptions and generates proper naddr links for cross-platform Web3 sharing.

import { nip19 } from "nostr-tools";

export function formatNostrEvent(event: any) {
  const findTag = (key: string) => event.tags.find((t: any) => t[0] === key)?.[1];

  const summary = findTag("summary");
  const fallbackDesc = event.content
    .replace(/![.*?](.*?)/g, "") 
    .replace(/[(.*?)](.*?)/g, "$1") 
    .replace(/(?:__|[*#`[]()-+!=])/g, "")
    .substring(0, 160)
    .trim() + "...";

  const dTag = findTag("d") || "";
  let naddr = "";
  try {
    naddr = nip19.naddrEncode({
      kind: 30023,
      pubkey: event.pubkey,
      identifier: dTag,
    });
  } catch (e) {
    console.error("Naddr error", e);
  }

  return {
    id: naddr || event.id,
    title: findTag("title") || "Untitled",
    description: summary || fallbackDesc,
    pubDatetime: new Date(event.created_at * 1000),
    modDatetime: null,
    author: findTag("author") || "Nostr User",
    tags: event.tags.filter((t: any) => t[0] === "t").map((t: any) => t[1]),
    ogImage: findTag("image"),
    content: event.content,
    naddr: naddr
  };
}

Phase 3: Building the Interface

We will hijack AstroPaper’s default routing to dynamically pull your Kind 30023 events at build time.

Step 1: The Homepage (index.astro)

Navigate to src/pages/index.astro. Delete everything and paste the code below. Replace the Hex string with your own public key.

---
import Layout from "@/layouts/Layout.astro";
import Header from "@/components/Header.astro";
import Footer from "@/components/Footer.astro";
import NDK, { type NDKEvent } from "@nostr-dev-kit/ndk";
import 'ws'; 

const ndk = new NDK({ 
  explicitRelayUrls: [
    "wss://relay.damus.io", 
    "wss://nos.lol",
    "wss://relay.primal.net"
  ] 
});

try {
  await ndk.connect(3000); 
} catch (err) {
  console.error("Nostr Connection failed:", err);
}

// REMEMBER TO INSERT YOUR HEX KEY BELOW
const filter = { 
  kinds: [30023], 
  authors: ["YOUR_HEX_PUBLIC_KEY_HERE"]
};

let events: Set<NDKEvent> = new Set();

try {
  const fetchPromise = ndk.fetchEvents(filter, { closeOnEose: true });
  const timeoutPromise = new Promise<Set<NDKEvent>>((resolve) => {
    setTimeout(() => resolve(new Set()), 4000);
  });

  events = await Promise.race([fetchPromise, timeoutPromise]);
} catch (err) {
  console.error("Fetch error:", err);
}

const posts = Array.from(events)
  .filter((e): e is NDKEvent & { created_at: number } => e.created_at !== undefined)
  .sort((a, b) => b.created_at - a.created_at)
  .map(e => {
    const cleanContent = e.content.replace(/[#*`_~[]()-]/g, "").replace(/s+/g, " ").trim();
    return {
      id: e.id,
      title: e.tags.find((t: string[]) => t[0] === 'title')?.[1] || "Untitled Post",
      summary: e.tags.find((t: string[]) => t[0] === 'summary')?.[1] || (cleanContent.substring(0, 120) + "..."),
      date: new Date(e.created_at * 1000).toLocaleDateString('en-US', {
        year: 'numeric', month: 'long', day: 'numeric'
      })
    };
  });
---

<Layout title="My Decentralized Mind-Dump">
  <Header />
  <main id="main-content" class="app-layout">
    <section id="hero" class="border-border border-b pt-8 pb-6">
      <h1 class="my-4 inline-block text-4xl font-bold sm:my-8 sm:text-5xl">My Nostr Mind-Dump</h1>
      <p>Pulled dynamically from the Nostr protocol at build time. Write on Web3, read everywhere.</p>
    </section>

    <section id="recent-posts" class="pt-12 pb-6">
      <h2 class="text-2xl font-semibold tracking-wide mb-6">Recent Notes</h2>
      <ul>
        {posts.map(post => (
          <li class="my-6">
            <a href={`/nostr/${post.id}`} class="inline-block text-xl font-bold text-skin-accent decoration-dashed underline-offset-4 hover:underline">
              {post.title}
            </a>
            <div class="text-sm opacity-80 mt-1">{post.date}</div>
            <p class="mt-2 line-clamp-3">{post.summary}</p>
          </li>
        ))}
      </ul>
    </section>
  </main>
  <Footer />
</Layout>

Step 2: The Article Reader Route

Create a new folder named nostr inside src/pages/. Inside that folder, create [id].astro. This generates dynamic paths for every article.

---
import Layout from "@/layouts/Layout.astro";
import Header from "@/components/Header.astro";
import Footer from "@/components/Footer.astro";
import NDK, { type NDKEvent } from "@nostr-dev-kit/ndk";
import 'ws';
import { parseNostrMarkdown } from "@/utils/parseMarkdown";
import { formatNostrEvent } from "@/utils/formatNostrEvent";

export async function getStaticPaths() {
  const ndk = new NDK({ explicitRelayUrls: ["wss://relay.damus.io", "wss://nos.lol"] });
  await ndk.connect(3000);

  let events: Set<NDKEvent> = new Set();

  // REMEMBER TO INSERT YOUR HEX KEY BELOW
  const filter = { kinds: [30023], authors: ["YOUR_HEX_PUBLIC_KEY_HERE"] };
  const fetchPromise = ndk.fetchEvents(filter, { closeOnEose: true });
  const timeoutPromise = new Promise<Set<NDKEvent>>((resolve) => setTimeout(() => resolve(new Set()), 4000));

  events = await Promise.race([fetchPromise, timeoutPromise]);

  return Array.from(events)
    .filter((e): e is NDKEvent & { created_at: number } => e.created_at !== undefined)
    .map(e => ({
      params: { id: e.id },
      props: { event: e }
    }));
}

interface Props { event: NDKEvent; }
const { event } = Astro.props;
const post = formatNostrEvent(event);
const htmlContent = await parseNostrMarkdown(post.content);
---

<Layout title={post.title}>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css" crossorigin="anonymous" />
  <Header />

  <main id="main-content" class="mx-auto w-full max-w-3xl px-4 pb-12 mt-8">
    <button class="focus-outline mb-6 flex hover:opacity-75" onclick="history.back()">
      <span>← Go back</span>
    </button>
    <h1 class="text-3xl font-bold text-skin-accent">{post.title}</h1>

    {post.naddr && (
      <div class="mt-6 flex flex-wrap items-center gap-3 text-sm bg-skin-card/10 p-4 rounded-lg border border-skin-line/50">
        <span class="font-bold">⚡ Nostr:</span>
        <a href={`nostr:${post.naddr}`} class="text-skin-accent hover:underline">Open in App</a>
        <a href={`https://yakihonne.com/article/${post.naddr}`} target="_blank" class="hover:text-skin-accent opacity-80">Yakihonne</a>
      </div>
    )}

    <article class="prose max-w-none prose-img:border-0 mt-8" set:html={htmlContent} />
  </main>
  <Footer />
</Layout>

<script>
  const renderMermaid = async () => {
    const codeBlocks = document.querySelectorAll("pre code.language-mermaid");
    if (codeBlocks.length === 0) return;
    const { default: mermaid } = await import("https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs");
    mermaid.initialize({ startOnLoad: false, theme: "dark" });

    for (const element of codeBlocks) {
      const id = `mermaid-${Math.random().toString(36).substr(2, 9)}`;
      const { svg } = await mermaid.render(id, element.textContent || "");
      const wrapper = document.createElement('div');
      wrapper.className = 'mermaid-container my-8 flex justify-center overflow-x-auto';
      wrapper.innerHTML = svg;
      element.parentElement?.replaceWith(wrapper);
    }
  };
  renderMermaid();
  document.addEventListener("astro:after-swap", renderMermaid);
</script>

Step 3: Global Styling and Contrast Fixes

Add these definitions to src/styles/global.css to fix dark mode contrast, ensure math blocks scroll gracefully on mobile, and apply proper borders to user-uploaded images.

/* Improve contrast for Dark Mode */
html[data-theme="dark"] .prose {
  --tw-prose-body: theme("colors.slate.200");
  --tw-prose-headings: theme("colors.white");
  --tw-prose-bold: theme("colors.white");
}

/* KaTeX Math Styling */
.katex-display {
  @apply my-8 overflow-x-auto py-4 px-2 rounded-lg;
  background-color: rgba(255, 255, 255, 0.03);
}

.katex {
  white-space: nowrap;
}

/* Nostr Image Attachments */
article img {
  @apply rounded-xl border shadow-md mx-auto my-8;
  border-color: var(--color-line); 
}

Start your local server by running npm run dev and opening http://localhost:4321. Your site should now dynamically render your Nostr notes.

Phase 4: AI-Native SEO (llm.txt)

As AI agents like Perplexity and SearchGPT replace traditional search engines, we must provide a semantic layer.

Create a file named llm.txt inside your public/ folder. Write a direct, markdown-formatted summary of your site’s core philosophy and architecture. When an AI scrapes yourwebsite.com/llm.txt, it immediately contextualizes your niche with zero token waste.

Phase 5: Deployment & The Zero-Maintenance Pipeline

It is time to put your site on the public internet and automate the build process.

  1. Push your code to a new public GitHub repository.
  2. Log into Vercel, click Add New Project, import your repository, and click Deploy. Vercel will grant you a live URL.

The Automation Webhook

Right now, your site is static. If you publish a new article on Nostr, your site won’t update until Vercel builds again.

  1. In your Vercel Dashboard, navigate to Settings -> Git -> Deploy Hooks.
  2. Create a hook named Nostr-Sync on the main branch and copy the generated URL.
  3. In your GitHub repository, go to the Actions tab and create a new workflow named nostr-sync.yml.
  4. Paste the following configuration, replacing the URL with your unique Vercel hook:
name: Nostr Auto-Sync

on:
  schedule:
    - cron: '0 */6 * * *'
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Trigger Vercel Build
        run: curl -X POST "https://api.vercel.com/v1/integrations/deploy/YOUR_UNIQUE_VERCEL_HOOK_URL"

Commit the changes.

You have successfully engineered an evergreen architecture. You write natively on Nostr, retaining absolute ownership of your cryptographic keys. Every 6 hours, GitHub Actions silently signals Vercel. Vercel scrapes the relays, compiles your latest thoughts into high-speed HTML, and serves it globally. Write once, syndicate everywhere.

How To Fix Common TypeScript Issues With Qodana

How to fix common TypeScript issues with Qodana

Most TypeScript projects already run ESLint with @typescript-eslint. That covers a lot: explicit any, floating promises, non-null assertions, and more. If your linting setup is solid, you’re catching the obvious issues in the editor before code review.

ESLint rules can’t produce cross-file findings. Each rule runs within a single file’s scope, which means ESLint can’t tell you that an export is unused everywhere in the codebase, that an any-typed value from one file is causing unsafe assumptions five files away, or that two components implement the same logic independently. That’s the gap Qodana fills.

Here are five TypeScript issues worth addressing, organized by what ESLint handles and where it runs out of scope.

Implicit any spreading through your codebase

ESLint’s no-explicit-any catches places where you write any. It doesn’t track what happens when any enters your codebase from external sources, such as from response.json(), a third-party library without types, or an untyped import. Once an externally-typed any value enters your code, it propagates silently through property accesses and function calls. ESLint’s no-unsafe-* rules can catch this, but only if you’re using @typescript-eslint/recommended-type-checked, which requires type-aware linting and is significantly less common than the standard recommended config.

async function getUser(id: string) {
  const res = await fetch(`/api/users/${id}`);
  const data = await res.json(); // type: any

  return data.profile.name; // no error — crashes if profile is undefined
}

response.json() returns any in the standard lib. Everything downstream is untyped. The compiler accepts any property name and any method call. The bug surfaces at runtime. Qodana tracks how any flows through the program across files. When an any-typed value reaches a code path where a specific shape is assumed, it flags the discrepancy, even if that’s several function calls away from where any entered the codebase.

Adding UserResponse doesn’t fix this. It just moves the lie closer to the crash. Type the boundary instead:

interface UserResponse {
  profile: { name: string };
}

const data: UserResponse = await res.json();

If the API response shape changes, the type error surfaces at compile time.

Non-null assertions used as shortcuts

ESLint’s no-non-null-assertion flags every ! operator uniformly. That works, but many teams disable the rule or add broad exceptions because legitimate uses, like after a runtime check, get flagged alongside the dangerous ones. The signal gets noisy, the rule gets turned off, and the problem disappears from view.

function renderUser(user: User | null) {
  return `Hello, ${user!.name}`; // crashes at runtime if user is null
}

const button = document.querySelector(".submit-btn");
button!.addEventListener("click", handleSubmit); // crashes if element doesn't exist

Both examples compile without errors. Both crash under predictable conditions. The ! is often added to silence a type error without fixing the underlying issue.

The correct approach is to handle the null case:

function renderUser(user: User | null) {
  if (!user) return "Guest";
  return `Hello, ${user.name}`;
}

const button = document.querySelector(".submit-btn");
if (button) {
  button.addEventListener("click", handleSubmit);
}

Qodana surfaces non-null assertions as a separate category in the report. Not every ! is wrong, but seeing them gathered in one place makes it easier to distinguish the legitimate uses from the shortcuts, without having to choose between a noisy rule and no rule at all.

Floating promises

ESLint’s @typescript-eslint/no-floating-promises is effective, but it’s a type-aware rule. It requires TypeScript type checking to be enabled in your ESLint config via parserOptions.project. In projects where that’s not configured, or configured only for part of the codebase, the rule silently doesn’t run on uncovered files.

async function onSubmit(data: FormData) {
  saveToDatabase(data); // Promise<void>, not awaited
  router.push("/success"); // runs before save completes
}

TypeScript accepts this without complaint. Calling an async function without await is considered valid syntax, and the return value is discarded. However, this behavior is incorrect: The user sees the success page before the save completes, and any database error is silently swallowed.

async function onSubmit(data: FormData) {
  await saveToDatabase(data);
  router.push("/success");
}

Qodana’s analysis is type-aware by default across the whole project, without requiring ESLint’s TypeScript integration to be separately configured. Floating promises get flagged consistently regardless of how the project’s ESLint setup is structured.

Unused exports

noUnusedLocals in tsconfig catches unused variables within a file. Exported symbols are excluded by design. From the compiler’s perspective, something outside the current file might import them. ESLint’s eslint-plugin-import provides an import/no-unused-modules rule that can detect this, but it requires scanning the entire dependency graph on every lint run and carries significant performance overhead on large codebases. For most projects, it’s not practical to keep it enabled.

// utils/format.ts
export function formatCurrency(n: number): string { ... }
export function formatPercent(n: number): string { ... } // removed feature, still here
export function formatBytes(n: number): string { ... }    // never imported anywhere

All three pass without a warning. But formatPercent and formatBytes are dead code. They add maintenance surface, slow down refactors, and mislead developers who assume exported symbols are in use.

Detecting this requires whole-project analysis. Qodana builds a reference graph across the entire codebase and tracks every import and re-export. Symbols that appear only as sources, never as import targets, get flagged. Neither tsc nor ESLint can do this.

Duplicated logic across files

ESLint doesn’t have native duplication detection. Standalone tools like jscpd exist for this, but they’re not part of your linting pipeline. That means separate setup, separate maintenance, and another thing to remember. The result: logic that gets copied between components or utility files accumulates without anyone flagging it.

// components/UserCard.tsx
function formatUserName(user: User): string {
  if (!user.firstName && !user.lastName) return "Anonymous";
  return [user.firstName, user.lastName].filter(Boolean).join(" ");
}
// components/UserBadge.tsx
function getDisplayName(user: User): string {
  if (!user.firstName && !user.lastName) return "Anonymous";
  return [user.firstName, user.lastName].filter(Boolean).join(" ");
}

This isn’t a style issue. It means bug fixes need to be applied in multiple places, and when they aren’t, behavior diverges silently between the two copies.

Qodana detects duplicated code across files as part of the same analysis pass that surfaces type issues and unused exports. When it appears in the report alongside everything else, it’s harder to deprioritize than a separate tool nobody remembers to run.

Setting up Qodana for your TypeScript project

All five issues above are visible in Qodana’s default profile for JavaScript and TypeScript projects. Here is a minimal qodana.yaml to get you started:

  version: "1.0"
  linter: jetbrains/qodana-js:2026.1                                                                                                                                                                                                             
  bootstrap: npm ci                                                                                                                                                                                                                              
  profile:                                                                                                                                                                                                                                       
    name: qodana.recommended                                                                                                                                                                                                                     
  failThreshold: 0                                                                                                                                                                                                                               
  exclude:        
    - name: All                                                                                                                                                                                                                                  
      paths:
        - dist                                                                                                                                                                                                                                   
        - node_modules

If the first run surfaces hundreds of existing issues, don’t let that block CI adoption. Qodana’s baseline feature captures the current state of the project in a qodana.sarif.json file. Commit it, and from that point on, CI only fails on newly introduced problems. The existing backlog stays visible in the report, but it doesn’t block every PR while you work through it.

Fix common TypeScript issues

Ready to fix common TypeScript issues with Qodana?

Try Qodana and let us know what you think.

Try Qodana Ultimate Plus

We’d like to extend a special thank-you to Qodana developer Lev Liadov for his contribution to this guide.

Mellum2 Goes Open Source: A Fast Model for AI Workflows

Trained from scratch and designed for practical deployment, Mellum2 is built for routing, Q&A, sub-agents, and private AI use in software engineering systems.

Today, we’re open-sourcing Mellum2, a 12B model engineered to solve the hardest parts of production AI: latency, throughput, and cost. Built from scratch and released under the Apache 2.0 license, Mellum2 offers a high-performance, cost-efficient alternative for your infrastructure.

Mellum began with code completion; now we’ve evolved it to handle both natural language and code. It is now a versatile tool ready to power routing, summarization, and intermediate reasoning steps across your modern AI workflows.

Whether you want to experiment, fine-tune, or deploy at scale, Mellum2 is ready to run in your own systems.

Try Mellum

Architecture and performance

Mellum2 is engineered to solve the bottlenecks of production-scale systems through its architecture and focused, efficiency-driven design.

  • Mixture-of-Experts (MoE) design: The model features 12B total parameters, but because it uses a MoE design, only 2.5B parameters are active per token. This reduces compute costs while enabling high-throughput, low-latency inference for real-time workloads.
  • Specialized focus: Unlike many modern models, Mellum2 is not multimodal. It is trained specifically on natural language and code data. This specialization ensures the model excels in software engineering environments while remaining lean and fast.

In our technical report, we detail our model’s performance across code generation, science, math, and reasoning benchmarks. Mellum2 is competitive with other similar-sized models while cutting inference time to less than half – a definitive advantage for production-grade deployments.

Key use cases for Mellum2

  • Route and orchestrate AI workloads: Use Mellum2 to analyze incoming prompts and help select the right model or tool for each task.
  • Build low-latency RAG pipelines: Retrieve relevant context, use Mellum2 to summarize it, and generate responses instantly.
  • Power fast sub-agents in complex workflows: Break down agent pipelines into steps like context gathering, planning, and validation. Use Mellum2 for fast, specialized tasks instead of relying on a single large model.
  • Enable private, local AI deployment: Run Mellum2 locally or self-host it to keep code and data fully under your control.

The “focal model” philosophy: Why focused models scale better

As AI systems become more complex, performance bottlenecks shift from raw capability to latency, throughput, and cost at scale. Not every task requires the largest model. Many steps in modern AI systems are repetitive, latency-sensitive, and high-frequency. These steps benefit from a fast and reliable model that can be efficiently routed, hosted, and controlled.

At JetBrains, we believe the future belongs to coordinated systems, not single models. Frontier models will continue to push the limits, but practical AI products also require focal models: fast, specialized components that handle high-frequency tasks efficiently.

That’s the role we see for Mellum2 in the next generation of AI software tooling.

Get started with Mellum2

If you’re building AI systems for software engineering – whether inside an IDE, in a RAG pipeline, as part of an agent workflow, or fully on your own infrastructure – we’d love for you to try Mellum2.

Open source is how better tools get made.

Try Mellum

June Is For Exploring (2026 Wallpapers Edition)

Everyone has a creative spark in them. Some bring their ideas to life with digital tools, others capture the perfect moment with a camera or love to grab pen and paper to create little doodles or pieces of lettering. And even if you don’t think of yourself as particularly creative, who knows? There might be a hidden talent waiting to be discovered!

That’s exactly what our monthly wallpapers series has been all about for over 15 years now. It’s a chance to step away from the everyday and dive into a fun, creative project. And this month is no different!

Talented artists and designers from all over the world have once again put their skills to work, creating unique and inspiring desktop wallpapers to brighten up your screens this June. You’ll find their designs below, along with some favorites from our archives that were just too good to leave out. A huge thank you to everyone who shared their creations with us this month — you’re smashing!

If you too would like to get featured in one of our upcoming wallpapers posts, please don’t hesitate to join in. We can’t wait to see what you’ll come up with! Happy June!

  • You can click on every image to see a larger preview.
  • We respect and carefully consider the ideas and motivation behind each and every artist’s work. This is why we give all artists the full freedom to explore their creativity and express emotions and experience through their works. This is also why the themes of the wallpapers weren’t anyhow influenced by us but rather designed from scratch by the artists themselves.

Drifting Into June

“June marks the beginning of summer and the end of the semester for many colleges. When I think of summer, I think of walks along the park with my family watching ducks swimming in the lake, and swimming in our pool with my siblings. Naturally from there I put them together to create an amusing scene of a duck slowly paddling around in an inner tube.” — Designed by Emma Kim from the United States.

  • preview
  • with calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440
  • without calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440

Let The Ocean Influence You

“The ocean covers more than 70% of the surface of the Earth, yet we know barely anything about it. Maybe June can be the month you discover something new about yourself.” — Designed by Ginger IT Solutions from Serbia.

  • preview
  • with calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440
  • without calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440

Dancing In The Kitchen

“June is such an iconic summer month, filled with sunshine, hope, and possibilities. When we raise both hands above our heads in celebratory dance, we symbolically release our cares for the day. When we do this dance in the kitchen, we not only release but also celebrate our bodies, our souls, and nourishment we’re about to give ourselves. This digital collage represents the freedom of movement, of dance, of joyful expression, of nourishment, creativity, and hope. May we all dance in the kitchen.” — Designed by Sue Jenkins from the United States.

  • preview
  • with calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440
  • without calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440

Fruit & Gingham

“June reminds me of fresh fruit and bright colors, so I decided to watercolor some fruits, and it complimented well on a blue gingham picnic table background!” — Designed by Ella Peplowski from Ringwood, NJ.

  • preview
  • with calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440
  • without calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440

Not In The Mood Forecast

Designed by Ricardo Gimenes from Spain.

  • preview
  • with calendar: 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1366×768, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440, 3840×2160
  • without calendar: 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1366×768, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440, 3840×2160

Ballpark Patches

“June brings in warmer weather, so I designed a baseball-patch-inspired piece to celebrate the start of the season and the nice weather.” — Designed by Madison Evans from Scranton, PA.

  • preview
  • with calendar: 320×480, 640×480, 800×480, 1024×768, 1280×720, 1280×800, 1280×960, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440
  • without calendar: 320×480, 640×480, 800×480, 1024×768, 1280×720, 1280×800, 1280×960, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440

A Very Special Amusement Park

“June brings summer, and it’s a great time to travel and experience new adventures! An amusement park is always a good idea, although some are best enjoyed from the comfort of your own home.” — Designed by Veronica Valenzuela from Spain.

  • preview
  • with calendar: 640×480, 800×480, 1024×768, 1280×720, 1280×800, 1440×900, 1600×1200, 1920×1080, 1920×1440, 2560×1440
  • without calendar: 640×480, 800×480, 1024×768, 1280×720, 1280×800, 1440×900, 1600×1200, 1920×1080, 1920×1440, 2560×1440

Wavy Jellyfish

Designed by Jayden Evans from Scranton, Pennsylvania.

  • preview
  • with calendar: 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440
  • without calendar: 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440

Buzzing Through June

“I first found this color palette and thought it was very springy, and when I think of spring I think of flowers and bees. So I wanted to create a design that incorporated both in a cute way.” — Designed by Caroline Flynn from the United States.

  • preview
  • with calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1920×1080, 1920×1200, 1920×1440, 2560×1440
  • without calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1920×1080, 1920×1200, 1920×1440, 2560×1440

Tiny Paradise Under the Sun

“Summer is hidden in the simple moments — the shimmer of crystal water, the warmth of the sun on your skin, and the calm sound of a peaceful afternoon. The warm wooden deck and soft shade of the parasol create a peaceful corner made for daydreaming. Flowers bloom, lemonade stays cold, and the sunlight dances across the pool like a golden melody. It’s the season of relaxation, happiness, and small moments that feel unforgettable.” — Designed by PopArt Studio from Novi Sad, Serbia.

  • preview
  • with calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440
  • without calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440

Patches Of Flowers

“I wanted to make something in Blender, and once I made these flowers I felt like it needed something more, so I added more texture and more colors to make these flowers more peaceful.” — Designed by Caroline Flynn from the United States.

  • preview
  • with calendar: 320×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440
  • without calendar: 320×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440

Amsterdam

“Inspired by the upcoming 2027 edition of SmashingConf Amsterdam.” — Designed by Ricardo Gimenes from Spain.

  • preview
  • with calendar: 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1366×768, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440, 3840×2160
  • without calendar: 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1366×768, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440, 3840×2160

Let Me Grow With You

Designed by James Lucia from Covington Township, Pennsylvania.

  • preview
  • with calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440, 3840×2160
  • without calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440, 3840×2160

Travel Time

“June is our favorite time of the year because the keenly anticipated sunny weather inspires us to travel. Stuck at the airport, waiting for our flight but still excited about wayfaring, we often start dreaming about the new places we are going to visit. Where will you travel to this summer? Wherever you go, we wish you a pleasant journey!” — Designed by PopArt Studio from Serbia.

  • preview
  • without calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440

June Is For Nature

“In this illustration, Earth is planting a little tree — taking care, smiling, doing its part. It’s a reminder that even small acts make a difference. Since World Environment Day falls in June, there’s no better time to give back to the planet.” — Designed by Ginger IT Solutions from Serbia.

  • preview
  • without calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1020, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440

Tastes Of June

“A vibrant June wallpaper featuring strawberries and fresh oranges, capturing the essence of early summer with bright colors and seasonal charm.” — Designed by Libra Fire from Serbia.

  • preview
  • without calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1366×768, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440

A Bibliophile’s Shelf

“Some of my favorite things to do are reading and listening to music. I know that there are a lot of people that also enjoy these hobbies, so I thought it would be a perfect thing to represent in my wallpaper.” — Designed by Cecelia Otis from the United States.

  • preview
  • without calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1200, 1920×1440, 2560×1440

Here Comes The Sun

Designed by Ricardo Gimenes from Spain.

  • preview
  • without calendar: 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1366×768, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440, 3840×2160

Create Your Own Path

“Nice weather has arrived! Clean the dust off your bike and explore your hometown from a different angle! Invite a friend or loved one and share the joy of cycling. Whether you decide to go for a city ride or a ride in nature, the time spent on a bicycle will make you feel free and happy. So don’t wait, take your bike and call your loved one because happiness is greater only when it is shared. Happy World Bike Day!” — Designed by PopArt Studio from Serbia.

  • preview
  • without calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1366×768, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440

Deep Dive

“Summer rains, sunny days, and a whole month to enjoy. Dive deep inside your passions and let them guide you.” — Designed by Ana Masnikosa from Belgrade, Serbia.

  • preview
  • without calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440

Oh, The Places You Will Go!

“In celebration of high school and college graduates ready to make their way in the world!” — Designed by Bri Loesch from the United States.

  • preview
  • without calendar: 320×480, 1024×768, 1280×1024, 1440×900, 1680×1050, 1680×1200, 1920×1440, 2560×1440

Merry-Go-Round

Designed by Xenia Latii from Germany.

  • preview
  • without calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1366×768, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440

Join The Wave

“The month of warmth and nice weather is finally here. We found inspiration in the World Oceans Day which occurs on June 8th and celebrates the wave of change worldwide. Join the wave and dive in!” — Designed by PopArt Studio from Serbia.

  • preview
  • without calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1366×768, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440

Summer Surf

“Summer vibes…” — Designed by Antun Hirsman from Croatia.

  • preview
  • without calendar: 640×480, 1152×864, 1280×1024, 1440×900, 1680×1050, 1920×1080, 1920×1440, 2650×1440

Summer Party

Designed by Ricardo Gimenes from Spain.

  • preview
  • without calendar: 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1366×768, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440, 3840×2160

Expand Your Horizons

“It’s summer! Go out, explore, expand your horizons!” — Designed by Dorvan Davoudi from Canada.

  • preview
  • without calendar: 800×480, 800×600, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1366×768, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440

Handmade Pony Gone Wild

“This piece was inspired by the My Little Pony cartoon series. Because those ponies irritated me so much as a kid, I always wanted to create a bad-ass pony.” — Designed by Zaheed Manuel from South Africa.

  • preview
  • without calendar: 800×600, 1024×768, 1280×960, 1280×1024, 1680×1050, 1920×1200, 2560×1440, 2880×1800

Pineapple Summer Pop

“I love creating fun and feminine illustrations and designs. I was inspired by juicy tropical pineapples to celebrate the start of summer.” — Designed by Brooke Glaser from Honolulu, Hawaii.

  • preview
  • without calendar: 640×480, 800×600, 1024×768, 1152×720, 1280×720, 1280×800, 1280×960, 1366×768, 1440×900, 1680×1050, 1920×1080, 1920×1200, 1920×1440, 2560×1440

All-Seeing Eye

Designed by Ricardo Gimenes from Spain.

  • preview
  • without calendar: 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1366×768, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440, 3840×2160

Nine Lives

“I grew up with cats around (and drawing them all the time). They are so funny… one moment they are being funny, the next they are reserved. If you have place in your life for a pet, adopt one today!” — Designed by Karen Frolo from the United States.

  • preview
  • without calendar: 1024×768, 1024×1024, 1280×800, 1280×960, 1280×1024, 1366×768, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440

Summer Coziness

“I’ve waited for this summer more than I waited for any other summer since I was a kid. I dream of watermelon, strawberries, and lots of colors.” — Designed by Kate Jameson from the United States.

  • preview
  • without calendar: 320×480, 1024×1024, 1280×720, 1680×1200, 1920×1080, 2560×1440

Bauhaus

“I created a screenprint of one of the most famous buildings from the Bauhaus architect Mies van der Rohe for you. So, enjoy the Barcelona Pavillon for your June wallpaper.” — Designed by Anne Korfmacher from Germany.

  • preview
  • without calendar: 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×800, 1280×960, 1280×1024, 1366×768, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440

Strawberry Fields

Designed by Nathalie Ouederni from France.

  • preview
  • without calendar: 320×480, 1024×768, 1280×1024, 1440×900, 1680×1200, 1920×1200, 2560×1440

Papa Merman

“Dream away for a little while to a land where June never ends. Imagine the ocean, feel the joy of a happy and carefree life with a scent of shrimps and a sound of waves all year round. Welcome to the world of Papa Merman!” — Designed by GraphicMama from Bulgaria.

  • preview
  • without calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1366×768, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440

Solstice Sunset

“June 21 marks the longest day of the year for the Northern Hemisphere — and sunsets like these will be getting earlier and earlier after that!” — Designed by James Mitchell from the United Kingdom.

  • preview
  • without calendar: 1280×720, 1280×800, 1366×768, 1440×900, 1680×1050, 1920×1080, 1920×1200, 2560×1440, 2880×1800

Getting Better Everyday

“Inspired by the eternal forward motion to get better and excel.” — Designed by Zachary Johnson-Medland from the United States.

  • preview
  • without calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1366×768, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1920×1080, 1920×1200, 1920×1440, 2560×1440

Reef Days

“June brings the start of summer full of bright colors, happy memories, and traveling. What better way to portray the goodness of summer than through an ocean folk art themed wallpaper. This statement wallpaper gives me feelings of summer and I hope to share that same feeling with others.” — Designed by Taylor Davidson from Kentucky.

  • preview
  • without calendar: 480×800, 1024×1024, 1242×2208, 1280×1024

Ice Creams Away!

“Summer is taking off with some magical ice cream hot air balloons.” — Designed by Sasha Endoh from Canada.

  • preview
  • without calendar: 320×480, 1024×768, 1152×864, 1280×800, 1280×960, 1400×1050, 1440×900, 1600×1200, 1680×1050, 1920×1080, 1920×1200, 2560×1440

Melting Away

Designed by Ricardo Gimenes from Spain.

  • preview
  • without calendar: 320×480, 640×480, 800×480, 800×600, 1024×768, 1024×1024, 1152×864, 1280×720, 1280×800, 1280×960, 1280×1024, 1400×1050, 1440×900, 1366×768, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440

Sunset With Crabs

“In the sunset, the crabs come to the surface. That little boat can’t sail, but after seeing the crabs it gets power and finally… it sails!” – Designed by Veronica Valenzuela from Spain.

  • preview
  • without calendar: 640×480, 800×480, 1024×768, 1280×720, 1280×800, 1440×900, 1600×1200, 1920×1080, 1920×1440, 2560×1440

World Environment Day

“On June 5th, we celebrate World Environment Day — a moment to pause and reflect on how we impact Earth’s health. A few activities represented in this visual include conserving energy and water, shopping and growing local, planting flowers and trees, and building a sustainable infrastructure.” — Designed by Mad Fish Digital from Portland, OR.

  • preview
  • without calendar: 320×480, 1024×1024, 1280×720, 1680×1200, 1920×1080, 2560×1440

Shine Your Light

“Shine your light, Before the fight, Just like the sun, Cause we don’t have to run.” — Designed by Anh Nguyet Tran from Vietnam.

  • preview
  • without calendar: 768×1280, 1024×1024, 1280×800, 1280×1024, 1366×768, 1440×900, 1600×1200, 1680×1050, 1680×1200, 1920×1080, 1920×1200, 1920×1440, 2560×1440

Get Featured Next Month

Feeling inspired? We’ll publish the July wallpapers on June 30, so if you’d like to be part of the collection, please don’t hesitate to submit your design. We are already looking forward to it!

A MOGONET-Style Multi-Omics Biomarker Pipeline: Why a Near-Random Graph Net Still Earns Its Place

TL;DR (Quick Answer)

This is an honest engineering write-up of a MOGONET-style multi-omics consensus biomarker pipeline built as an internal R&D project at sysofti.

  • The headline — on a small synthetic cohort (n=30), the graph network alone scores near-random in leak-free 5-fold cross-validation (AUC 0.53 ± 0.16). Yet as one voter in a 5-evidence consensus, the top-10 ranking is 90% real markers (9 of 10 are known periodontitis genes).
  • The lesson — a single model that looks weak in honest evaluation can still be a useful voter. That contrast is the whole point of the consensus design, and we show it with data.
  • What it is — per-omics Graph Convolutional Networks (GCN) over a sample-similarity graph, attention-fused, contributing to a consensus score alongside differential-expression hubs, Random Forest, a DNN, and co-expression modules.
  • What it is *not* — the official MOGONET. We dropped the original’s VCDN fusion for attention fusion. Call it “MOGONET-based.” All numbers are from synthetic data with embedded ground-truth markers — code validation, not a clinical claim.

If you’re implementing multi-omics integration, the parts you can’t get from the paper are below: the real results, the leakage-aware evaluation, and the bugs we hit.

What MOGONET Is (the One-Line Mental Model)

MOGONET (Multi-Omics Graph cOnvolutional NETwork) learns a separate GCN per omics view on a sample-similarity graph (patients as nodes, edges by feature similarity), then fuses the per-view embeddings for classification and biomarker discovery. Reference: Wang et al. 2021, Nature Communications 12:3445; the GCN itself is Kipf & Welling 2017.

Mental model: “build one graph net per omics layer, let each form an opinion, then combine those opinions.”

What We Simplified — and Why

The original MOGONET fuses views with a View Correlation Discovery Network (VCDN). We replaced it with attention-weighted fusion:

  • Why — with tiny cohorts (tens of samples), VCDN’s extra parameters were a liability; attention fusion gave a simpler intermediate-fusion scheme that still up-weights the more informative omics per sample.
  • The tradeoff — we lose the explicit cross-view correlation modeling that is part of MOGONET’s original contribution. So this is honestly MOGONET-based, not a reimplementation. The source docstring says as much: “Simplified implementation of MOGONET.”

Architecture

Input: X_views = [omics1 (n×p1), omics2 (n×p2), ...]   (n = common samples)
  └─ per-view StandardScaler
  └─ per-view k-NN (cosine) adjacency  (n×n)
ViewEncoder (per omics):  GraphConv(p→128) → BN → ReLU → GraphConv(128→64)
  → view embedding (n×64)
Attention fusion:  softmax(Linear(64→1)) over views → weighted sum (n×64)
Classifier:  Linear(64→32) → ReLU → Linear(32→n_classes)
class GraphConvLayer(nn.Module):
    def __init__(self, in_features, out_features):
        super().__init__()
        self.linear = nn.Linear(in_features, out_features)
    def forward(self, x, adj):
        return torch.mm(adj, self.linear(x))   # propagate over the sample graph

class MOGONET(nn.Module):
    def __init__(self, input_dims, hidden_dim=128, latent_dim=64, n_classes=2):
        super().__init__()
        self.encoders = nn.ModuleList([ViewEncoder(d, hidden_dim, latent_dim) for d in input_dims])
        self.attention = nn.Linear(latent_dim, 1)
        self.classifier = nn.Sequential(nn.Linear(latent_dim, 32), nn.ReLU(), nn.Linear(32, n_classes))
    def forward(self, views, adjs):
        embeddings = [enc(x, adj) for enc, x, adj in zip(self.encoders, views, adjs)]
        stacked = torch.stack(embeddings, dim=0)                       # n_views × n × latent
        attn = F.softmax(self.attention(stacked).squeeze(-1), dim=0)   # per-view, per-sample
        fused = (stacked * attn.unsqueeze(-1)).sum(dim=0)              # n × latent
        return self.classifier(fused)

Sample-similarity graph — k-NN (cosine), no self-loops on purpose (see below):

def build_adjacency(X, k=5):
    sim = cosine_similarity(X)
    adj = np.zeros_like(sim)
    for i in range(len(sim)):
        top_k = np.argsort(sim[i])[-k-1:-1]      # top-k neighbours, excluding self
        adj[i, top_k] = sim[i, top_k]
        adj[top_k, i] = sim[top_k, i]            # symmetrize
    row_sum = adj.sum(axis=1, keepdims=True); row_sum[row_sum == 0] = 1   # guard zero-sum rows
    return adj / row_sum

The Engineering Decisions That Mattered

  • Sample-node graph, not feature graph. Nodes are patients; edges are patient-patient similarity. Same-group patients cluster, so the GCN smooths group signal.
  • No self-loops — on purpose. Standard GCN uses Ahat = A + I so a node keeps its own features. We deliberately omit the self-loop so each node’s representation is built purely from its sample-neighborhood, pushing the model toward group structure rather than individual raw features. It is a tradeoff (you give up the node’s own signal each layer), and we flag it as a choice, not an accident.
  • Per-view scaling + common-sample intersection. Each omics standardized independently; only samples present in all views are used.
  • Consensus over a single model. MOGONET is one of five evidence sources by design — Hub (DE+PPI), ML (Random Forest), DL (DNN), WGCNA co-expression, and MOGONET — with a multi-evidence bonus:
avg_score = sum(scores.values()) / max(len(scores), 1)
composite = avg_score * (1 + 0.3 * (n_sources - 1))   # reward agreement across sources

As the results show, this design choice is what makes the pipeline useful despite any single model being weak.

Results (Synthetic Data, with Ground Truth)

We validate on a synthetic periodontitis case-control set (3 omics — transcriptomics 500, proteomics 200, metabolomics 100 features × 30 samples, 15 disease / 15 control, seed-fixed) with known biomarkers deliberately embedded: up-regulated inflammatory genes (MMP8, MMP9, IL1B, IL6, TNF, RANKL, CTSK, TLR4 …) and down-regulated bone-formation genes (COL1A1, RUNX2, SP7, BGLAP, OPG …). Embedding known markers gives ground truth — you can check whether the pipeline recovers them, which is impossible on a real cohort.

Note on sources: the pipeline defines five evidence sources, but in this run WGCNA returned no co-expression hubs, so four sources actually contributed (Hub, ML, DL, MOGONET).

The consensus ranking surfaces real markers

Of 793 candidate features, the top-30 consensus included 13 of the 25 embedded markers. The ranking is strikingly clean at the top:

Top-20 consensus biomarkers, bar length = composite score, color = number of supporting evidence sources, star = known periodontitis marker

Rank Gene Composite Sources Known marker
1 MMP8 1.888 4
2 COL1A1 1.212 3
3 MMP9 1.020 4
4 IL6 1.000 1
5 IL1B 0.900 4
6 METAB_0031 0.866 1
7 TLR4 0.856 3
8 RANKL 0.838 3
9 CTSK 0.803 3
10 SP7 0.678 3
11 MYD88 0.672 3
  • Precision@10 = 0.90 — 9 of the top 10 are known markers (only METAB_0031 is not).
  • Recall@10 = 0.36, Recall@20 = 0.52 (9 then 13 of 25 known markers); it plateaus by 20 because a few embedded markers were given weak synthetic signal (e.g. TNF, fold-change ≈ 1.1).

More evidence = more trustworthy

Breaking the top-30 down by which sources agreed makes the consensus logic concrete:

Evidence-source combinations among the top-30 consensus genes, and how many in each group are known markers

  • 4 sources → 3 genes, all 3 known (100%): MMP8, MMP9, IL1B.
  • 3 sources → 17 genes, 9 known.
  • 2 sources (DL + MOGONET) → 8 genes, 0 known — pure noise.
  • 1 source → 2 genes, 1 known.

The signal lives where independent methods agree. A gene flagged by four sources was always real here; genes flagged by only two were not.

The honest part: the graph net alone is near-random

We cross-validated MOGONET as a standalone classifier, rebuilding the sample graph from training folds only to avoid leakage:

MOGONET 5-fold CV AUC = 0.53 ± 0.16 (folds: 0.44, 0.44, 0.78, 0.33, 0.67)

That is barely above chance. With n=30 (six test samples per fold) and a transductive sample-graph model, a single GCN simply cannot generalize here — and its training AUC near 1.0 is mostly the leakage and the injected signal talking. This is exactly why MOGONET is wired in as one voter, not the decision-maker. The consensus result above is strong because it doesn’t trust any single model, including this one.

Honest Limitations

  1. Simplified model. No VCDN fusion — attention instead. “MOGONET-based,” not a reimplementation.
  2. MOGONET is a weak standalone classifier here (CV AUC 0.53). Useful only in aggregate. It also scores all 793 features, so its solo discriminative power is low.
  3. Synthetic, small (n=30). Results validate the code’s ability to recover injected signal — not clinical performance. External cohorts are required for any real claim.
  4. Single run (seed 42). Known markers are stable at the top; the unnamed GENE_xxxx candidates shuffle on re-runs.
  5. Self-loop omission is a design choice with a cost — worth A/B testing against the standard A + I formulation.
  6. Feature importance is an approximation (first-layer weight magnitude), not a gradient-based attribution.

What Broke Along the Way (Real Notes)

  • Zero-sum adjacency → NaN. If a sample’s k-NN cosine similarities summed to zero, row-normalization divided by zero and propagated NaNs. Fixed with a row_sum[row_sum == 0] = 1 guard.
  • Attribute-name mismatches (fixed twice). Pulling feature importance broke on AttributeError when the sklearn-wrapper conventions clashed with the nn.Module attribute names (view_encodersencoders, modelmodel_).
  • Common-sample collapse. When omics measured different sample sets, the intersection shrank fast. Added a “≥6 common samples” guard that skips gracefully instead of crashing.
  • MOGONET scores everything. It assigns weight to all 793 features, so it appeared in all top-30 entries — the multi-evidence bonus is what keeps it honest.

What We’d Improve Next

  • Report consensus performance under the same leak-free CV, not just MOGONET’s.
  • A/B test self-loops (Ahat = A + I).
  • Gradient-based attribution (Integrated Gradients) instead of first-layer weights.
  • Add VCDN fusion and compare head-to-head with attention fusion.
  • External multi-omics cohort for real-world validation.

FAQ

Q: Is this the official MOGONET implementation?

No — a simplified, MOGONET-based design: per-omics GCN with attention fusion, without the original’s VCDN view-correlation network.

Q: If MOGONET’s CV AUC is only 0.53, why keep it?

Because it is one voter in a five-source consensus, not the classifier. Single models overfit small cohorts; consensus rewards agreement across independent methods, and that ranking recovered known markers at 90% precision in the top 10. A weak voter still adds signal when combined.

Q: Why validate on synthetic data?

Embedded known markers give ground truth, so you can measure recovery (recall/precision) — impossible on a real cohort where the answer is unknown. It validates the code, not clinical utility.

Q: Why omit GCN self-loops?

Intentional: without a self-loop, each node’s representation comes purely from its sample-neighborhood, pushing the model toward group structure rather than individual features. It is a tradeoff worth A/B testing, not a universal recommendation.

Q: Can I use this on my own multi-omics data?

Yes — the classifier is sklearn-compatible (fit/predict/predict_proba). Build the sample graph from training data only to avoid leakage, and don’t over-read AUC on small cohorts.

Resources

  • Reference implementation (clean, standalone, MIT): github.com/shoo99/mogonet_lite
  • Original paper: Wang T. et al. (2021), MOGONET integrates multi-omics data via graph convolutional networks for biomarker discovery, Nat Commun 12:3445.