SIGCOT is the system a power-transmission operations center uses to plan, carry out and record what happens on the grid: every intervention on a piece of equipment, every switching plan, every occurrence on the shift. It is a Java desktop program that runs on the operators’ workstations and, since September 2026, stores everything in a corporate SQL Server. Before that, it stored everything in a Microsoft Access file opened over the network.
This technical case shows how the system is built, how it changed databases while operations depended on it, and which real failures changed the project’s rules. The numbers were checked in the code, in the version of 29/10/2026.
Transparency: SIGCOT was designed and is developed by Jeremias Cardoso, the author of this blog, who works at the operations center that uses the system. The text does not name the company, people, servers or real equipment. Infrastructure security details are also left out.
Update note (30/10/2026): this post was rewritten as a technical case. The 18/09/2026 version described the system in general terms; this one brings the architecture, the database migration, the incidents, the limits and the numbers checked in the repository.
The problem: a routine that cannot lose a record
At the center of the routine is the Intervention Request (SI). It describes what will be done on a piece of equipment, when, by whom and with which safety precautions. It then passes through six stages: in preparation, forwarded, under review, scheduling, execution and completed. The other operating documents turn around it: the Switching Plan, the authorizations to perform work and to take equipment out of service, the operating messages, the daily occurrence report and the disturbance report. The intervention types follow the network procedures of the National Electric System Operator (ONS).
Before the system brought everything together, each part lived in its own place. The shift schedule was another program, with its own login and database. Even today, the notice for the message groups comes out of the system ready, but the last step is still copy and paste. Tracking of the variable parcel, the deduction from the transmission company’s revenue when a piece of equipment is unavailable, came from a spreadsheet with 35 tabs and about 18 thousand formulas, and gained its own screens in the system in October.
The requirement that runs through everything is easy to state and hard to meet: nothing can be lost in silence. An SI that the operator saw saved and that is not in the database is worse than an error on the screen, because nobody will go looking for it. That requirement explains almost every decision below, and almost every incident too. It is the same silent failure that brings down ordinary automations, in a place where it costs more — and the same insistence on recording the fact before trusting the screen that shows up in GradUP when an exam has to survive an audit.
The system in numbers
All numbers are from October 2026, with the date of each measurement in parentheses.
| Measure | Value |
|---|---|
| Application code (Java, screens and styles) | about 205 thousand lines (29/10) |
| Test code | about 127 thousand lines (29/10) |
| Scripts for the migration to SQL Server | about 11 thousand lines (29/10) |
| Java files for the application and the screens | 822 and 80 (29/10) |
| Automated tests | 5,580, no failures (27/10) |
| Versions in the history | 174, from 2.0.05 to 2.13.1 (25/10) |
| Workstations registered in the fleet | 61 (26/10) |
| Database | 142 tables, 297,323 rows (25/10) |
The line count considers only versioned files and excludes blank lines and comments. In the application there are 180 thousand lines of Java, 14 thousand of screens in FXML, the JavaFX screen format, and 10 thousand of CSS styles. With blanks and comments, it passes 288 thousand physical lines.
The database, on the other hand, is small: fewer than 300 thousand rows and about 5.5 MB in the backup of 25/10. What makes the system hard is not data volume. It is people saving at the same time, from different places, in a process where a lost record has consequences.
Architecture: a desktop program on dozens of workstations
SIGCOT is written in Java 21 with JavaFX, the Java graphical interface library, and built with Maven. It runs on each operator’s Windows, not in the browser. The operations center already had the workstations, and the system stays open for the whole shift, reacting to events without reloading a page. Since September 2026, each workstation has its own local installation and updates itself: the system warns when there is a new version and makes the switch. The published package is signed, and the updater refuses a file that does not match.
Three architecture decisions are worth recording.
A single database address for everyone. At the start, each workstation read the database address from its own configuration file. Two workstations ended up opening different databases, and the result looked like missing data. Today the address comes from a central pointer, with a backup copy and a local cache as a last resort. In an emergency, a workstation can point at another database, but that switch is never written to the central pointer and undoes itself.
Concurrency is between processes, not between threads. More than fifteen workstations write to the same database, and each one is a separate process that does not share memory with the others. The project rule is direct: a test that runs everything in a single process does not cover the real case. On SQL Server there are tests with truly concurrent workstations. In one of them, four workstations register the same occurrence at the same instant, and a database lock lets only one through.
No database work on the UI thread. In JavaFX, a single thread draws the screen. If it waits on a query over the VPN, the screen freezes. An inventory on 22/06/2026 found 297 places that did this kind of waiting on the UI thread. The pattern adopted sends the work to a background thread and returns the result only if the screen still shows the same record. A watchdog writes to the log, with the call stack, every time the UI thread stays stuck for more than 200 milliseconds.
The architecture also has debt, and the repository itself records it. A code review described the real pattern as an eroded MVC, with screen controllers of thousands of lines. The largest has close to 7,900 lines in a single file.
From Access to SQL Server with operations in use
Until 11/09/2026, the SIGCOT database was an Access file in a network folder. Anyone working from outside the office opened, over the VPN, a 19 MB file. The project documentation treated 15 simultaneous clients as a hard limit of the shared file, and the file called for the periodic ritual of compact and repair. In July, the project already recorded as a closed decision that the real cure was to change the database.
The plan that fell in nine days
The migration plan was written on 18/08/2026. On 27/08, when IT sent the connection manuals, the premise fell on four points. The three that weighed most: the database would not be dedicated to SIGCOT, but shared with other systems; the version was SQL Server 2019; and the collation, the rule the database uses to compare text, was already set. The system’s boundary stopped being the whole database and became a schema, the area of the database reserved for it.
Two measurements made in that phase show why checking is worth more than assuming:
- A column with no declared collation inherits the database’s. In two databases identical except on that point, the same search for a name with a tilde, typed without the tilde, returned 0 rows in one and 1 in the other. Declaring the collation on the column fixes it.
- Access “true” is not SQL Server’s. A query that compared a logical field with -1, the value of true in Access, compiled on the server and found nothing. The variable parcel would have come out R$ 0.00. The fix went in on 04/09, with a test that scans the entire code for the pattern.
Translate on the connection instead of rewriting
The queries were spread across 98 files. Instead of rewriting each one, the project put a central translator on the connection: it adds the schema to the table names and swaps the Access dialect for the SQL Server dialect, such as Now() for GETDATE(). To prove that it worked, a tool extracted each SQL phrase from the Java code itself and compiled it, without executing, on a local SQL Server with the 99 tables. Of 727 phrases, 716 compiled, 8 were inconclusive and 3 failed. None of the three failures belonged to the translator.
A pit stop, not a tire change with the car moving
The temptation was to keep both databases alive and synchronize underneath during the transition. The plan rejected that in writing: an SI saved in Access after the last extraction would cease to exist, with no error and no log.
What was done was to rehearse on a copy, with production continuing on Access. On 06/09, a rehearsal load took 47,619 rows in 87 tables to the server. The cutover was a short pit stop, on the night of 11/09: everyone out of the system, a final snapshot of the file, a reload, a check by count and a release. Each step of the runbook ended with a code, and the next step ran only on code zero.
That same night, the operations center started working on version 2.0.146, already on SQL Server. On 14/09, field staff started coming in through the installer. On 16/09 came the decision to retire Access: new Access versions would not come back, and the frozen file became history. On 22/09, the shift schedule, which still lived in a separate Access database, was loaded: 21,989 rows in a single transaction, checked source against destination.
The rite for changing the database structure
With the database on the server, changing a table became the riskiest moment in the project. The rules that stayed:
- The application does not change the database structure. In server mode, the program does not issue DDL, the commands that create or alter tables. The decision is in an architecture record, ADR-009. The first version of it was rejected in the project’s own review, with scores between 7.5 and 8.7 against a cutoff of 9.5, and it was rewritten.
- Every change is a numbered script, which can run twice without damaging anything, validated first on a local SQL Server 2019. A person applies it, with a tool that asks for typed confirmation and refuses, before connecting, any command outside the SIGCOT area. Right after, a second tool checks the result object by object.
- The field is born in the database before the version that uses it. Code that expects a column that does not exist yet breaks in production.
- Text width is decided generously at creation. Text the user types freely goes in as
NVARCHAR(MAX). Widening it later costs a script and a window; being born wide costs nothing. - The application checks the database on open, against a contract of what it expects to find. If something it requires is missing, it does not open.
The last rule looks like mere caution. On 18/09, it took down the entire system, as the next section shows.
Incidents that became rules
Almost every rule above has a story. Six of them:
31/07/2026: the SI stopped saving. The operator saw a permission error in a Java license folder, with no relation to the SI. There were two errors stacked. The one on top came from the database driver, which, while building the error message, scanned the disk and crashed. The one underneath was the real one: the equipment name in a column limited to 100 characters. The fix made the error message safe in 17 places in the code. The lesson went into the rite: width is decided at creation. When the new database was created, free text was already born with no limit.
03/08/2026: three hours without getting into the system. The author himself tried 27 times to repoint the database, going through 5 different files. The cause was triple. The package shipped without the version recorded, which reduced 459 error occurrences to a single “Unexpected network error”. The file check only asked whether it existed and could be read. And a single message covered four different causes. Version 2.0.130 started testing the file for real and saying which cause had happened.
07/08/2026: an SI saved, read back and then lost. The screen confirmed it, the notice went out on the team channel, and even so the next four update commands hit zero rows. The investigation measured the size of the problem: one unrecovered SI in 671, over two months. The response was proportional. The update started requiring exactly one affected row, inside a transaction, and the screen gained three distinct outcomes instead of a generic “saved”.
19/08/2026: the daily report “stopped”. The automatic sending of the report stopped arriving after the automation hub came online, and the record kept saying it had sent. The cause was in the send: the script started by ignoring every error, and the program returned success because it had managed to open the process, not because the email went out. The fix was to check the exit code of each step.
02/09/2026: outages two to three times a day. A presence signal, written every minute by each workstation, forced the others to reload the entire database. Version 2.0.142 removed the signal.
18/09/2026: the script that took down its own version. A disturbance record with more than one transmission function would not save, because of a column-width defect latent since June. A script widened four columns and ran at 20:30, with code zero in the application and in the check. Right after, no workstation would open version 2.0.147. That version’s structure checker blocked any difference in column size, including a larger column. And the update check ran only after it, so it never got to run. The script’s runbook stated that a wider column would not get in the way, and nobody had checked that against the checker.
The way out was to install 2.0.148 workstation by workstation. 2.0.149 brought a checker that blocks only what actually makes the program wrong, and it started checking the version on the login screen, before touching the database. The rule that stayed: no column change while a workstation is on an old version.
The common thread is the same in almost every case: the system thought it was right. Each fix traded a silent failure for a visible failure, or for a written rule.
Tests that lock decisions
The suite grew with the system: 980 tests on 22/07/2026, 1,863 on 17/08, 2,817 on 10/09, the day before the cutover, 4,380 in version 2.9.0 and 5,580 on 27/10, with no failures.
About 100 test classes are contract guards. Instead of testing a function, they lock a decision already made. One checks that every screen actually loads. Another checks that the SQL scripts only touch the SIGCOT area. Another measures label coverage for a screen reader and fails if it falls below the floor. A test can be skipped only if it is on a list with a reason, an owner and an expiration date. The project also recorded a lesson about the guards themselves: a guard turned off by a condition is not a guard, because the report stays green claiming coverage that never ran.
Publishing follows the same logic as gates that prove. The release script fails closed by default: it compiles once, runs the suite, generates the component list, looks for known vulnerabilities in the dependencies and seals the package with a hash. If any step fails, nothing is generated. The release happens in two stages: the minimum required version rises only after another workstation has proved that it updated itself. Before and after each release, a reading kit, queries only, photographs the database for comparison.
What is still unresolved
An honest technical case includes what is missing:
- Access is still in the code. It left the field in September, but its driver remains in the project and part of the suite still runs on it. Removal of the binary is planned in phases.
- Dates stored as text. In the survey for the Access retirement plan, 121 date and time columns were text, against 51 with a date type. The conversion is planned and has not started.
- Giant controllers. The reorganization into modules was proposed in an architecture record and was not carried out.
- Partial accessibility. Label coverage for a screen reader rose from 4% on 01/08 to 47% on 26/09, with the six most-used screens at 100%. The screen-by-screen work on the rest is still missing, along with full keyboard navigation.
- The database does not roll back. There is no database rollback: restoring a backup of the shared server would affect other systems. That is why everything that changes the database goes through a rehearsal first.
- No measure of gain. The repository counts workstations, sessions and rows, but it does not measure time saved or SIs per day. This text does not claim a productivity gain because that number does not exist.
Lessons that hold outside the electric sector
- Measure the infrastructure before trusting the plan. The plan of 18/08 was wrong on four points, and only the IT manuals and the check on the real database showed that. Finding out at the cutover would have cost operations.
- Do not synchronize two living systems. Rehearse on a copy as many times as needed and make the switch with everyone out, checking by count.
- Trade a silent failure for a visible one. A message that hides the cause, a generic “saved” and a script that returns success without checking are at the origin of the worst incidents.
- Check each guard against the version that is in the field. The checker that protected the database took the system down because nobody tested it against the change that was coming.
- Write the rule on the day of the incident. “Width is decided at creation” and “no column change with an old workstation in the field” are written in the repository. It is the written rule that prevents a repeat, not the memory of whoever was there.
Sources
- Architecture, migration timeline, incidents, tests and line count: the SIGCOT repository in the version of 29/10/2026, read by the author; extract with the passages, the counting method and the limits of the verification, verified on 2026-10-30.
- Network procedures and intervention types at transmission facilities: National Electric System Operator (ONS) (sector documentation), verified on 2026-09-18.



