miercuri, 20 august 2025

Debugging Hotel Wi-Fi Login on Arch Linux (and a Docker Gotcha)

Recently, while staying at a hotel, I tried connecting my Arch Linux laptop to the Wi-Fi. As usual, the network redirected to a captive portal login page. But this time, things didn’t work as expected.

First, I checked the captive portal detection URL with:

curl -I http://detectportal.firefox.com

That gave me a gateway URL like:

http://127.17.0.1/login.php

But when I tried to open it in a browser, I kept getting “Unable to connect”.

At first, I thought the hotel’s Wi-Fi was broken. Then I noticed something odd: the IP address started with 127.17…. That reminded me of Docker, since I had seen a similar subnet in docker info.

So I ran:

ip addr show

And sure enough, I saw this:

docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:84:b7:e7:1e brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:84ff:feb7:e71e/64 scope link proto kernel_ll

Aha! Docker was hijacking the IP range 172.17.x.x, which happened to be the same network the hotel’s captive portal was using.

To fix it, I temporarily disabled the Docker bridge:

sudo ip link set docker0 down
sudo ip addr flush dev docker0

After that, I could finally access:

http://172.17.0.1/login.php

and log in to the Wi-Fi.

marți, 12 august 2025

Quickly Format a List for SQL IN Clause Using Notepad++

Let’s say I have a list of strings:

STRING1
STRING2
...
STRING200

And I want to create a query like:

SELECT count(*) 
FROM TABLE 
WHERE COLUMN IN ('STRING1', 'STRING2', ..., 'STRING200');

Quick solution using Notepad++:

  1. Open the file in Notepad++.
  2. In the Find what field, enter:
    ^(.*)$
    
  3. In the Replace with field, enter:
    '$1',
  4. Set Search Mode to Regular expression.
  5. Click Replace All.
This will wrap each string in quotes and add a trailing comma, so you can paste it directly into your SQL IN clause.

joi, 24 iulie 2025

VS Code "Open Folder" Does Nothing on Arch Linux (LXQt)

I installed Visual Studio Code on Arch Linux with LXQt, but clicking "Open Folder" did nothing — no error, no folder picker, just silence.

To debug, I ran:

journalctl -f

Then clicked "Open Folder" again. This time, I saw the error:

xdg-desktop-portal: Backend call failed: Could not activate remote peer 'org.freedesktop.impl.portal.desktop.gtk': activation request failed: unit is masked

Fix:

systemctl --user unmask xdg-desktop-portal-gtk.service

systemctl --user enable --now xdg-desktop-portal-gtk.service

vineri, 6 iunie 2025

Unexpected Behavior: PL/pgSQL Block Ran Forever the First Time

 I once encountered a curious situation where a particular DO $$ BEGIN ... END $$; block in PostgreSQL executed forever the first time I ran it. But then — surprisingly — every subsequent execution was relatively fast and uneventful.

The same behavior repeated when I deployed to a new environment: the first run took ages, and then all future runs were fast again.


CREATE TABLE a (...);

DO BEGIN $$
  INSERT INTO a  ... -- many many rows, e.g. 1 million

  UPDATE a ...
  FROM (SELECT a ... JOIN b ... GROUB BY ...) AS c
  WHERE a.some_column=c.some_column; 

END $$;
The solution was to add : ANALYZE a; right after the INSERT INTO a statement.

I was interested in seeing the query plan, and I got an idea from ChatGPT on how to display the result of EXPLAIN inside a DO BEGIN END block.

DO 
DECLARE
  q text;
  r text;
BEGIN $$
  INSERT INTO a  ... -- many many rows, e.g. 1 million

  q = 'UPDATE a ...
  FROM (SELECT a ... JOIN b ... GROUB BY ...) AS c
  WHERE a.some_column=c.some_column';

  FOR r IN EXECUTE 'EXPLAIN ' || q
  LOOP
    RAISE NOTICE '%', r;
  END LOOP;
END $$;
The query plan for the first version

NOTICE:  Update on a  (cost=2355271.49..1300407104.89 rows=0 width=0)
NOTICE:    ->  Nested Loop  (cost=2355271.49..1300407104.89 rows=858585 width=86)
NOTICE:          Join Filter: (a.some_column = c.some_column)
NOTICE:          ->  Seq Scan on a  (cost=0.00..16320.85 rows=858585 width=14)
NOTICE:          ->  Materialize  (cost=2355271.49..1297825633.93 rows=200 width=104)
NOTICE:                ->  Subquery Scan on c  (cost=2355271.49..1297825632.93 rows=200 width=104)
i.e for each row form a there will be a loop in c.

The query plan after ANALYZE


NOTICE:  Update on a  (cost=682681.45..725641.23 rows=0 width=0)
NOTICE:    ->  Hash Join  (cost=682681.45..725641.23 rows=200 width=86)
NOTICE:          Hash Cond: (a.some_column = c.some_column)
NOTICE:          ->  Seq Scan on a  (cost=0.00..37022.45 rows=2261645 width=14)
NOTICE:          ->  Hash  (cost=682678.95..682678.95 rows=200 width=104)
NOTICE:                ->  Subquery Scan on c  (cost=682674.45..682678.95 rows=200 width=104)

vineri, 23 mai 2025

Cleaner HTTP Request Assertions with AssertJ

I switched to using AssertJ's fluent API with .satisfies() for verifying MockWebServer requests—and I love it. It keeps the code concise and readable, and best of all, I didn’t need to define an extra variable just to run my assertions.

Original (JUnit-style):

RecordedRequest request = mockWebServer.takeRequest();
assertEquals("/graph", request.getPath());
assertTrue(request.getBody().readUtf8().contains("mutation AddSomething"));

Improved (AssertJ-style):

assertThat(mockWebServer.takeRequest()).satisfies(request -> {
    assertThat(request.getPath()).isEqualTo("/graph");
    assertThat(request.getBody().readUtf8()).contains("mutation AddSomething");
});

marți, 20 mai 2025

Extracting First Names from full_name with LLaMA 3 and Ollama in Kotlin

Assume we have a users table containing the columns full_name, last_name, and first_name. In 10% of the cases, the first_name field is NULL, but we observe that the first name is embedded within the full_name field, just without a clear delimiter (e.g., 'DUMBRAVEANURADU'). Or we can make the problem more difficult by considering compound first names, which are also not separated by spaces from the last name (e.g., 'IONESCUANDREIMIHAIL', 'POPESCOANAMARIA', 'VASILESCUIONAELENA', 'TOMAIOANDANIEL', 'MOLDOVANMARIAGABRIELA').

I would like to tell my experience of solving such a problem.

1. Export all rows where first_name is NULL into a data.csv file (PgAdmin > Save results to file).

2. I started Ollama and loaded the LLaMA 3 model locally

docker run -d -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama
docker exec -it 57b53e2bc095 ollama pull llama3 
3. I Created a Kotlin Project and Asked ChatGPT for Code Fragments
build.gradle.kts
dependencies {
    implementation("com.squareup.okhttp3:okhttp:4.12.0")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.17.0")
    implementation("com.fasterxml.jackson.core:jackson-databind:2.17.0")
    testImplementation(kotlin("test"))
}

4. I Asked ChatGPT How to Construct the Prompt
Main.kt
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import java.io.File
import java.io.PrintWriter
import java.util.concurrent.TimeUnit

fun main() {
    val client = OkHttpClient.Builder()
        .connectTimeout(10, TimeUnit.SECONDS)  // Time to establish connection
        .readTimeout(30, TimeUnit.SECONDS)     // Time to wait for server response
        .writeTimeout(15, TimeUnit.SECONDS)    // Time to send request data
        .build()

    val mapper = jacksonObjectMapper()

    val inputStream = object {}.javaClass.getResourceAsStream("/data-1746619055858.csv")
        ?: error("File not found in resources")

    val writer = PrintWriter(File("output.csv"))

    writer.println(listOf("id", "full_name", "last_name", "first_name", "sql")
        .joinToString(",") { "\"$it\"" })

    inputStream.bufferedReader().useLines { lines ->
        lines
            .drop(1) // Skip the header line
            .take(100)
            .forEach {
                val columns = it.split(",")
                    .map { it.trim('"') } // Remove surrounding quotes

                val id = columns[0]
                val fullName = columns[1]
                
                println("ID: $id, Name: $fullName")

                val json = mapper.writeValueAsString(
                    mapOf(
                        "model" to "llama3",
//                        "system" to "The following string contains a full name in uppercase without clear delimiters between the last name(s) and the first name(s).\nPlease analyze the name and split it into two parts:\n\nlastName: which can include compound surnames.\n\nfirstName: whichANDREI-MIHAIL may also be a compound name.\nTry to detect where the first name begins even if it is attached directly to the last name (e.g., DUMBRAVEANURADU → DUMBRAVEANU + RADU).\nReturn the result in JSON format like:\n{ \"lastName\": \"...\", \"firstName\": \"...\" }",
                        "system" to """
                            The following string contains a full name in uppercase without clear delimiters between the last name(s) and the first name(s).
                            Your task is to analyze and split it into two parts:
                            - "lastName: which can include compound surnames"
                            - "firstName: which may also be a compound name
                            Try to detect where the first name begins even if it is attached directly to the last name (e.g., DUMBRAVEANURADU → DUMBRAVEANU + RADU).     
                            Important: Return only a JSON object with the following format:
                            { "lastName": "...", "firstName": "..." }
                            Do not explain or add any other text. Only return valid JSON.
                           
                            Examples:
                            - "DUMBRAVEANURADU" should be split into "lastName": "DUMBRAVEANU", "firstName": "RADU"
                            - "IONESCUANDREIMIHAIL" should be split into "lastName": "IONESCU", "firstName": "ANDREI-MIHAIL"
                            - "POPESCOANAMARIA" should be split into "lastName": "POPESCO", "firstName": "ANA-MARIA"                                                       
                        """.trimIndent(),
//                        "prompt" to participantName,
                        "prompt" to "Name: $participantName\nReturn JSON only.",
                        "stream" to false
                    )
                )
                

                val request = Request.Builder()
                    .url("http://localhost:11434/api/generate")
                    .post(json.toRequestBody("application/json".toMediaType()))
                    .build()

                client.newCall(request).execute().use { response ->
                    if (!response.isSuccessful) {
                        println("Unexpected response: $response")
                    } else {
                        val body = response.body?.string()
                        val parsed = mapper.readValue<Map<String, Any>>(body!!)
                        println(parsed["response"])
                        val response =  mapper.readValue<Map<String, Any>>(parsed["response"].toString())
                        val lastName = response?.get("lastName")
                        val firstName = response?.get("firstName")

                        val sql = """
                            UPDATE users
                            SET last_name = '$lastName',
                                first_name = '$firstName'
                            WHERE id = '$id' AND full_name = '$fullName';
                        """.trimIndent().replace("\n", " ")
                        writer.println(listOf(caseNumber, participantName, response?.get("lastName"), response?.get("firstName"), sql)
                            .joinToString(",") { "\"$it\"" })
                    }
                }
            }
    }

    writer.close()
}

5. Execute and Analyze the Output File

vineri, 16 mai 2025

Generating Structured Output with OpenAI Java SDK

When working with OpenAI's API responses in Java, it's often useful to request structured output instead of plain text. This enables better parsing, validation, and downstream processing. The OpenAI Java SDK supports this through JSON Schema-based formats.

Reference: StructuredOutputsExample.java (GitHub) 

https://github.com/openai/openai-java/blob/main/openai-java-example/src/main/java/com/openai/example/StructuredOutputsExample.java

            ResponseFormatTextJsonSchemaConfig.Schema schema = ResponseFormatTextJsonSchemaConfig.Schema.builder()
                    .putAdditionalProperty("type", JsonValue.from("object"))
                    .putAdditionalProperty(
                        "properties", JsonValue.from(Map.of("employees", Map.of("type", "array", "items", Map.of("type", "string")))))
                    .putAdditionalProperty("required", JsonValue.from(List.of("employees")))
                    .putAdditionalProperty("additionalProperties", JsonValue.from(false))
                    .build();
            createParams = createParams.toBuilder()
//                .instructions(assistant.instructions())
                .text(ResponseTextConfig.builder()
                    .format(ResponseFormatTextJsonSchemaConfig.builder()
                        .name("employee-list")
                        .schema(schema)
                        .build())
                    .build())
                .build();
                
        openAiClient.getClient().responses().create(createParams).output().stream()
                .flatMap(item -> item.message().stream())
                .flatMap(message -> message.content().stream())
                .flatMap(content -> content.outputText().stream())
                .forEach(outputText -> System.err.println(outputText.text()));       

Convert Assistant defined response format. 

Assistant assistant = openAiClient.getClient().beta()
            .assistants()
            .retrieve(AssistantRetrieveParams.builder()
            .assistantId(openAiAssistantId)
                .build());
ResponseFormatJsonSchema.JsonSchema jsonSchema = assistant.responseFormat().get()
                .asResponseFormatJsonSchema()
                .jsonSchema();
                
                
ResponseFormatTextJsonSchemaConfig.Schema schema = ResponseFormatTextJsonSchemaConfig.Schema.builder()
                .additionalProperties(jsonSchema.schema().get()._additionalProperties())
                .build();
        ResponseCreateParams createParams = ResponseCreateParams.builder()
            .input(input)
            .model(model)
            .text(ResponseTextConfig.builder()
                .format(ResponseFormatTextJsonSchemaConfig.builder()
                    .name(jsonSchema.name())
                    .schema(schema)
                    .build())
                .build())
            .build();