.NET Deployment Issue: Ghost Dependency

Symptom

  • .NET 8 app with Polly fails on deploy: Could not load file or assembly 'Microsoft.Extensions.Http, Version=9.0.0.0'

Checks

  1. Dependencies

    • No preview packages in .csproj
    • Locked Microsoft.Extensions.Http to 8.0.0
    • Local build clean → Not a package issue
  2. Environment

    • CI/CD fixed to .NET 8 SDK
    • Cleared ~/.dotnet and ~/.nuget
    • Removed caches → Environment clean

Root Cause

  • Deploy script only overwrote files, never cleaned target dir
  • Old HappyNotes.Api.deps.json misled runtime to request v9.0.0.0

Lesson Always wipe target dir before publish:

rm -rf /your/target/directory/*

Desktop Mouse Swipe Delete Troubleshooting

Problem

Mouse left swipe to delete notes was not working on desktop platforms (Windows, macOS, Linux, Web browsers).

Root Cause Analysis

  1. Flutter Dismissible Limitations: Dismissible widget is optimized for touch interactions, not mouse gestures
  2. SelectionArea Gesture Conflict: SelectionArea intercepted mouse drag events for text selection, blocking Dismissible swipe gestures

Solution

Initial Approach (Failed)

Attempted platform-specific custom GestureDetector implementation:

  • Added desktop platform detection logic
  • Implemented custom mouse drag threshold calculations
  • Issue: GestureDetector only detects gestures but provides no visual feedback

Final Solution (Working)

Two-part fix for Dismissible widget:

  1. Immediate Mouse Response
Dismissible(
  dragStartBehavior: DragStartBehavior.down, // Key fix
  // ... other properties
)
  1. Gesture Conflict Resolution
// Before: SelectionArea wrapping entire content
SelectionArea(
  child: GestureDetector(...) // Blocked swipe gestures
)

// After: SelectionArea only around text content
GestureDetector(
  child: Column([
    metadata,
    SelectionArea(child: noteContent), // Limited scope
    footer,
  ])
)

Technical Details

dragStartBehavior Impact

  • DragStartBehavior.start (default): Waits for drag distance threshold
  • DragStartBehavior.down: Starts drag immediately on mouse down
  • Desktop users expect immediate response to mouse actions

SelectionArea Scope Reduction

  • Problem: Full-content SelectionArea captured all mouse events
  • Solution: Limit SelectionArea to text content only
  • Result: Swipe gestures work on margins, text selection works on content

Testing Strategy

Created comprehensive test suite covering:

  • Dismissible configuration validation
  • Platform-specific behavior
  • Gesture conflict scenarios
  • Integration with existing callbacks

Key Learnings

  1. Flutter's Dismissible supports desktop with proper configuration
  2. Widget event hierarchies can cause unexpected gesture conflicts
  3. Scope reduction often beats complex custom implementations
  4. Platform-specific UX requires careful gesture management

Code Impact

  • Files Modified: note_list_item.dart
  • Tests Added: note_list_item_test.dart
  • Lines Changed: 30 additions, 15 deletions
  • Breaking Changes: None

Verification

  • All tests pass
  • Code analysis clean
  • Desktop mouse swipe delete functional
  • Text selection preserved

GitHub CLI Multi-Account Auto-Switcher: Zero-Config Solution

If you juggle work and personal GitHub accounts like I do, constantly checking which account is active before running gh pr create gets old fast. Here's a perfect solution that completely eliminates this friction.

The Problem

Working with multiple GitHub accounts means:

  • Forgetting which account is currently active
  • Getting "No default remote repository" errors
  • Manually running gh auth switch and gh repo set-default
  • Accidentally creating PRs with the wrong account

The Solution

Create a gh wrapper script that automatically detects the current repository's owner, switches to the correct GitHub account, and sets the default repository before executing any command.

Implementation

#!/bin/bash

# Path to original gh binary
ORIGINAL_GH="/usr/local/bin/gh"  # Adjust for your system

# Get current repository's remote URL
remote_url=$(git remote get-url origin 2>/dev/null)

# If not in a git repo, pass through to original gh
if [ $? -ne 0 ]; then
    exec "$ORIGINAL_GH" "$@"
fi

# Extract full repository name (owner/repo) - remove .git suffix
if [[ $remote_url =~ github\.com[:/]([^/]+/[^/]+) ]]; then
    repo_full_name="${BASH_REMATCH[1]}"
    repo_full_name="${repo_full_name%.git}"
    repo_owner=$(echo "$repo_full_name" | cut -d'/' -f1)
else
    exec "$ORIGINAL_GH" "$@"
fi

# Get current active GitHub account
current_user=$("$ORIGINAL_GH" api user --jq '.login' 2>/dev/null)

if [ $? -ne 0 ]; then
    exec "$ORIGINAL_GH" "$@"
fi

# Switch account if needed
if [ "$repo_owner" != "$current_user" ]; then
    echo "→ Repository belongs to $repo_owner, switching from $current_user..." >&2
    "$ORIGINAL_GH" auth switch >/dev/null 2>&1
fi

# Set default repository to avoid "No default remote repository" errors
"$ORIGINAL_GH" repo set-default "$repo_full_name" >/dev/null 2>&1

# Execute original command
exec "$ORIGINAL_GH" "$@"

Setup

  1. Find your original gh path:
which gh
# Use this path in the ORIGINAL_GH variable
  1. Create the wrapper script:
# Save script as ~/.local/bin/gh
chmod +x ~/.local/bin/gh
  1. Adjust PATH priority:
# Add to ~/.bashrc or ~/.zshrc
export PATH="$HOME/.local/bin:$PATH"
  1. Verify setup:
source ~/.bashrc
which gh  # Should show ~/.local/bin/gh

Usage

All GitHub CLI commands now automatically use the correct account:

# In personal repository
gh pr create  # Uses personal account, sets correct default repo

# In work repository  
gh pr create  # Uses work account, sets correct default repo

# All other commands work transparently
gh issue list
gh pr view
gh repo clone username/repo

Advanced: Working with Forks

For forked repositories where you want to view PRs in the upstream repo:

# View your PRs in the upstream repository
gh pr list --author @me --repo upstream-owner/repo-name

# Or temporarily switch to upstream
gh repo set-default upstream-owner/repo-name
gh pr view [PR_NUMBER]

How It Works

  • Extracts repository owner from the current directory's origin remote URL
  • Compares repository owner with currently active GitHub account
  • Automatically runs gh auth switch if accounts don't match
  • Always sets the correct default repository to prevent CLI errors
  • Transparently proxies all other gh commands

Benefits

  • Zero configuration required - works out of the box
  • Zero habit changes needed - still use gh commands normally
  • Eliminates common errors - no more "No default remote repository" messages
  • Multi-account friction eliminated - never think about which account is active again

Perfect for developers who work across multiple GitHub organizations or maintain both work and personal projects.

Mocking Node.js Path Separators: The Dependency Injection Solution

The Problem

Node.js path utilities like path.sep, path.join() are hardcoded to the current platform and readonly - you can't mock them for cross-platform testing:

// Instead of this brittle approach:
function createTempFile(name: string, separator: string) {
  return 'tmp' + separator + name; // Manual string manipulation
}

// Use dependency injection:
function createTempFile(name: string, pathImpl = path) {
  return pathImpl.join('tmp', name);
}

// Now you can test both platforms reliably:
createTempFile('data.json', path.win32)  // → tmp\data.json
createTempFile('data.json', path.posix)  // → tmp/data.json

Why This Works

Node.js provides path.win32 and path.posix as separate implementations. Instead of fighting the platform dependency, embrace it through clean dependency injection. Test Windows logic on Linux, test Unix logic on Windows - no mocking needed.

网友语录 - 第44期 - 能有人共享日出、晚霞、满月、漂亮的云,是很幸福的事

这里记录我的一周分享,通常在周六或周日发布。


小青 如果你想要的东西很明确,很确定,那去往那个终点的困难是有且有限的求饶、僵等、躲避,都过不去。行则将至


郝靠谱 能有人共享日出、晚霞、满月、漂亮的云,是很幸福的事


刘子超:有的人走着走着就忘了自己为什么走,就一直往前走。(这不就是人生吗)

许子东:汉族数千年的农耕文化,以及儒家的传统,都是一亩三分地。(人们骨子/基因里害怕远行,但内心又向往远行。因此看别人远行就是某种程度的还愿了。)

想到三毛,没人会像他这样去生活,(但)很多人愿意看她这样生活。

摘自圆桌派一期刘子超谈他中亚旅行的节目


Marskay 一件值得做的事情即使做的不怎么样也是值得的


三猫 一个基本的事实:人类能够承受的压力等级(持续时间和量级)是有限的。任何人放在持续高压的应激环境里,都会变得易怒、哭泣、好斗、抑郁、失去理性。人是血肉做的,是很容易被摧毁的。一个人之所以正常,是因为ta处在正常的环境中,而不是因为ta天生是个正常人。


长毛象野生动物园 流水线工厂的工作,在纽约时报的报道描述中属于“重体力劳动”

不过在中国打工圈的普遍语境中,这属于“轻体力工作”(重体力的是矿工、建筑工、农民之类的),因为这种工作对人的体力要求其实不高(不然古今中外也不会有那么多工厂童工了)。

虽然轻体力,但是这种工作对于精神的负荷极其巨大:以我的亲身经历为例,几年前打工那会儿,有一天某岗位缺人,我被分配到了一个组装遥控器的流水线上。(这种流水线工厂没有所谓固定岗位,工人随时会因为哪儿缺人而被安排换条流水线)我们这条线的工人每人被分配到了组装四千八百多个遥控器的任务。这是什么概念呢?当时我粗略估算了一下:我厂两班倒轮替,每班工作12小时,我是夜班,凌晨12点有30分钟吃饭时间,凌晨4点有15分钟休息时间。实际工作时间11小时15分钟,也就是四万秒出头。平均需要在8秒左右组装一个遥控器。(这还没计算上上厕所之类的时间)那款遥控器的组装并不是把两片遥控器的正反壳子一拼就完事了的(如果那样,啪叽两声就可以拼好一个,8秒太宽裕),遥控器生产全流程到了我们手上时有三个部件:除了正反两片塑料壳子,还有软胶质的小颗粒(遥控器上的按钮),我需要先把按钮嵌进遥控器正面壳子预留的镂空里,然后再把反面扣好。而按钮是极软的,用力稍大就会穿过镂空从另一边掉出来,用力小了又嵌不进去。必须用巧力刚刚好把按钮留在正面壳上,然后安装反面时还不能让按钮掉出来。刚开始我甚至半天都组装不好一个,总是会让按钮掉出来。而我却需要在几乎连续的11小时15分钟内,用我的双手,把同一套组装遥控器的动作,循环重复四千八百多次。

我的同一片肌肉,需要把同一类发力,反复做四千八百多次,每次需要在八秒之内做完。肌肉在工作后期的劳累,在这儿反而是次要的(虽然把同一小片肌肉重复使用四千多次还是很恐怖)。重要的是,我的大脑必须要集中精力,在内心把这同一套流程思考四千多次。这比把同一个单词抄写四千次恐怖多了。毕竟单词写多了有肌肉记忆,后期脑子不动也能写出来。组装遥控器不太行,走神了就组装不准。我身边的工友们,各个都是身经百战的,不像我这种打工菜鸟。有的人是组装得飞快,但是每天到了上班后期也是精神累大于身体累(而且肌肉的疲劳也会反馈给精神),许多人都打瞌睡。那一天我累死累活干到快下班,也只组装了两千多个遥控器。(让我震撼的是,我身边有的工友居然提前几个小时就做完了一天的...,做完以后开始摸鱼磨洋工……有人最终组装了五千多个)


最后临近下班时,是其中一个好心的工友送了我几百个组装好的遥控器,让我当成我的产量交上去交差,帮我凑够了三千六百多个的及格线,才让我那天勉强混过关……
(当时我感动得都要哭出来了。我跟那个工友几乎完全不认识……连话都没有说过几次,他却把自己的工作产出一下子送给我那么多)
(我对我的工友们的印象是,普遍都长着一张极其老实巴交的脸,性格也普遍特别老实。后来我想明白,不老实的人根本没有那种心力长期在工厂里吃这个苦。我进厂后最大的收获之一就是打破了我对农民工的各种刻板印象。)
那次打短工经历以后,我就特别特别理解三和大神为什么宁愿睡大街、吃挂壁面,也要多休息,也要少打工。


奧斯陸 花两百块钱买了个 3M 的强力降噪耳机(图中 X5A),真牛逼,戴上瞬间跟聋了一样,里面也有空间再戴个 AirPods Pro,这下真的清净了...


在森林里看花晒太阳的小熊 我有权优先照顾自己的需求和生活,就像树根有权优先滋养自己而非相邻的灌木


造物主知道有太多事情不能两全, 所以给浮生们留下了做梦的空间


我们只担心学生使用 AI 做作业,事实上,更应该担心的是教师使用 AI

很多老师为了节省时间,正在用 AI 快速生成低质量、毫无意义的 PPT 和讲义。


我这辈子认识的聪明人,没有一个是不大量读书的,一个也没有。巴菲特的阅读量之大,会让你大吃一惊。我的孩子们甚至嘲笑我,是一本伸出几条腿的书。

-- 芒格


虽然说对于装睡的人谁也叫不醒,但持续输出的话总能影响到那些有救的人


他过来诋毁我,其实是对我的一种仰视

周海媚