diff options
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | AGENTS.md | 88 | ||||
| -rw-r--r-- | CLAUDE.md | 8 | ||||
| -rw-r--r-- | LICENSE | 338 | ||||
| -rw-r--r-- | README.md | 85 | ||||
| -rw-r--r-- | homepage-services.yaml | 60 | ||||
| -rw-r--r-- | homepage-settings.yaml | 15 | ||||
| -rw-r--r-- | slackware-changelog.service | 20 | ||||
| -rwxr-xr-x | slackware_changelog.py | 197 | ||||
| -rw-r--r-- | test_changelog.py | 86 |
10 files changed, 899 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..4a13a10 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,88 @@ +# slackware-changelog + +A widget for a [Homepage](https://gethomepage.dev) dashboard, one of the +modules under `homepage-modules/`. Siblings share the shape described below. + +## Shape of a module + +Homepage's `customapi` widget fetches JSON and maps fields to labels. It +cannot parse text, cannot do arithmetic, and refetches on every refresh +interval. So each module is a small stdlib Python HTTP service on localhost +that does the parsing and caching, plus: + +- `<name>.py` - the service, single file, stdlib only, executable +- `test_<name>.py` - self-check, fakes the upstream, prints `ok` +- `<name>.service` - systemd unit, `DynamicUser=yes`, config via `Environment=` +- `homepage-services.yaml` / `homepage-settings.yaml` - snippets to merge + +No dependencies. No framework. `ThreadingHTTPServer` and `urllib`. + +## This module + +Serves the latest Slackware -current ChangeLog entry on `127.0.0.1:8098`. + +Endpoints: `/latest` (counts and summary), `/top` (the first `TOP_N` packages +as flat fields), `/packages` (every package with its action), `/entry` (HTML +page of the entry), `/refresh` (bypass the cache). + +### Why /top exists + +`customapi` maps a fixed list of field names to labels. It cannot iterate an +array and cannot scroll, so a JSON list of packages renders as nothing. `/top` +flattens the first `TOP_N` into `pkg1`, `pkg2`, ... which the widget can map +one per row, and appends a `... and N more` row in the next slot when the entry +is longer. The card links to the ChangeLog for the rest. + +Mapping more `pkgN` fields than the entry has packages is harmless: Homepage +skips a field that is absent. So the snippet maps a fixed 9 rows regardless of +how many that day's entry holds. + +### ChangeLog format + +The file is reverse-chronological. Entries are separated by a `+--------+` +line, so the latest entry is everything before the first separator. + +Inside an entry: + +``` +Mon Sep 7 22:56:39 UTC 2026 +a/util-linux-2.42.3-x86_64-1.txz: Upgraded. + This update fixes bugs and security issues. + (* Security fix *) +l/cryptopp-8.9.0-x86_64-3.txz: Rebuilt. + Fixed library location. Thanks to Petri Kaukasoina. +``` + +Gotcha: a package line is identified by **starting at column 0**, not by +ending in a period. Note lines are indented and frequently end in a period +too ("Thanks to Petri Kaukasoina."), so a period-based regex counts prose as +packages. The test asserts this specific case. + +Actions seen: `Upgraded`, `Rebuilt`, `Added`, `Removed`. The parser counts +whatever word it finds rather than matching a fixed list, so a new action +verb shows up in `summary` instead of vanishing. + +Security fixes are marked by the literal `(* Security fix *)` on its own +indented line. CVE ids appear in the notes as cve.org URLs. + +### Caching + +`CACHE_TTL` (default 1800s) governs upstream fetches; the widget's +`refreshInterval` only redraws. A failed refresh keeps the previous entry and +adds a `stale` field with the error, so an unreachable mirror degrades to an +old entry rather than a blank card. `/refresh` forces a fetch but obeys the +same fallback. + +### Rendering + +`/entry` escapes everything from the ChangeLog with `html.escape` before it +reaches the page. Upstream text is not trusted markup; the test asserts no +`<script` survives. + +## Conventions + +- GPLv2 only. Header notice in every source file. +- Snippet YAML files are merged into Homepage's config by hand, never copied + over it; the comments at the top of each say so. +- Layout groups: any group omitted from `layout:` falls to the bottom of the + page, so the snippet reminds the reader to list their existing groups too. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..daa4f3e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,8 @@ +# slackware-changelog + +See @AGENTS.md for everything: modules, endpoints, deployment and the +ChangeLog parsing gotchas. + +This file is intentionally thin. AGENTS.md is the single source of truth, +shared across every agent tool. Do not duplicate content here, edit AGENTS.md +instead. @@ -0,0 +1,338 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + <https://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, see <https://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Moe Ghoul>, 1 April 1989 + Moe Ghoul, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ded0877 --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +# slackware-changelog + +Shows the latest [Slackware -current](https://mirrors.slackware.com/slackware/slackware64-current/ChangeLog.txt) +ChangeLog entry: date, how many packages moved, and how many carry a security +fix. + +Homepage's `customapi` widget speaks JSON only, and the ChangeLog is plain +text: several thousand lines covering years of entries, of which only the +first block is wanted. This proxy fetches it, parses the latest entry, caches +it so the widget refresh does not hit the mirror, and serves the fields. + +Stdlib Python, no dependencies, one file. + +## Features + +- Counts per action: upgraded, rebuilt, added, removed +- Security fixes counted, and the CVE ids of the entry collected +- The full entry as a readable page, linked from the card +- A second card listing the packages themselves, truncated with a "... and N more" row +- An unreachable mirror serves the last good entry, flagged as stale, rather than a blank card + +## Endpoints + +| Path | Returns | +|---|---| +| `/latest` (or `/`) | Date, counts, one-line summary, CVE ids | +| `/top` | The first `TOP_N` packages as flat `pkg1..pkgN` fields, plus an overflow row. `?n=` overrides | +| `/packages` | Every package in the entry, with its action | +| `/entry` | The full entry as HTML | +| `/refresh` | Same as `/latest`, ignoring the cache | + +## Install + +Runs on the machine hosting Homepage, listening on `127.0.0.1:8098`. + +```bash +install -Dm755 slackware_changelog.py /opt/slackware-changelog/slackware_changelog.py +install -Dm644 slackware-changelog.service /etc/systemd/system/slackware-changelog.service +# edit the unit if you want a nearer mirror +systemctl daemon-reload && systemctl enable --now slackware-changelog +curl -s localhost:8098/latest +``` + +Then merge `homepage-services.yaml` into Homepage's `services.yaml` and the +`layout:` block from `homepage-settings.yaml` into its `settings.yaml`, and +restart Homepage. + +## Configuration + +Environment variables, set in the unit file: + +| Variable | Default | Meaning | +|---|---|---| +| `CHANGELOG_URL` | `https://mirrors.slackware.com/.../slackware64-current/ChangeLog.txt` | Use a nearer mirror, or `slackware-current` for 32-bit | +| `PORT` | `8098` | Listen port, bound to localhost | +| `CACHE_TTL` | `1800` | Seconds before the mirror is fetched again | +| `TOP_N` | `8` | Packages listed by `/top` before it truncates | + +## Parsing + +An entry runs from the top of the file to the first `+----+` separator line. +Inside it, a package line starts at column 0 and reads `path/name.txz: Action.`; +indented lines below it are free-text notes, which is why the parser anchors +on the column and not on the trailing period. Notes are kept in the entry text +but never counted as packages. + +## Tests + +```bash +python3 test_changelog.py +``` + +Fakes the mirror, so it needs no network. Prints `ok`. + +## License + +GPLv2. See [LICENSE](LICENSE). + +## Development Approach + +This project is developed using AI-assisted tools. Code is generated with the help of AI based on human-provided specifications, design decisions, and iterative feedback. + +All contributions are reviewed, tested, and curated by the maintainer before being included in the codebase. AI is used as a productivity and exploration tool, while human oversight remains central to all decisions. + +The goal is to combine the flexibility of AI-assisted development with standard open-source practices such as transparency, review, and accountability. diff --git a/homepage-services.yaml b/homepage-services.yaml new file mode 100644 index 0000000..9a2a0de --- /dev/null +++ b/homepage-services.yaml @@ -0,0 +1,60 @@ +# Append to Homepage's services.yaml. +# +# The href points at /entry, the full ChangeLog entry rendered as a page, so +# the card is worth clicking. Refresh is 30 min; the proxy caches upstream for +# CACHE_TTL anyway, so a shorter interval only redraws the widget. +# +# Two cards: a summary, and a package list. The list is truncated to TOP_N +# (8 by default, `?n=` overrides per widget) with a "... and N more" row, +# because customapi renders a fixed set of mapped fields and cannot scroll. +# The full list is one click away on the ChangeLog itself. + +- Slackware: + - ChangeLog current: + icon: slackware.png + href: http://127.0.0.1:8098/entry + description: Ultimo aggiornamento di -current + widget: + type: customapi + url: http://127.0.0.1:8098/latest + refreshInterval: 1800000 + display: list + mappings: + - field: date + label: Data + - field: summary + label: Modifiche + - field: packages + label: Pacchetti + format: number + - field: security + label: Fix di sicurezza + format: number + - Pacchetti aggiornati: + icon: slackware.png + href: https://mirrors.slackware.com/slackware/slackware64-current/ChangeLog.txt + description: Elenco completo sul ChangeLog + widget: + type: customapi + url: http://127.0.0.1:8098/top + refreshInterval: 1800000 + display: list + mappings: + - field: pkg1 + label: "1" + - field: pkg2 + label: "2" + - field: pkg3 + label: "3" + - field: pkg4 + label: "4" + - field: pkg5 + label: "5" + - field: pkg6 + label: "6" + - field: pkg7 + label: "7" + - field: pkg8 + label: "8" + - field: pkg9 + label: "" diff --git a/homepage-settings.yaml b/homepage-settings.yaml new file mode 100644 index 0000000..98b3bdd --- /dev/null +++ b/homepage-settings.yaml @@ -0,0 +1,15 @@ +# Merge this `layout:` block into Homepage's settings.yaml. +# +# Listing a group here also fixes its order on the page. Any group omitted +# falls to the bottom, so list the groups you already have too. + +layout: + Slackware: + style: row + columns: 2 # summary and package list side by side + + # Your other groups go here too, in the order you want them shown: + # + # Nome Gruppo: + # style: row + # columns: 3 diff --git a/slackware-changelog.service b/slackware-changelog.service new file mode 100644 index 0000000..fbaaf5f --- /dev/null +++ b/slackware-changelog.service @@ -0,0 +1,20 @@ +[Unit] +Description=Slackware -current ChangeLog proxy for Homepage +After=network-online.target +Wants=network-online.target + +[Service] +ExecStart=/usr/bin/python3 /opt/slackware-changelog/slackware_changelog.py +Environment=CHANGELOG_URL=https://mirrors.slackware.com/slackware/slackware64-current/ChangeLog.txt +Environment=PORT=8098 +Environment=CACHE_TTL=1800 +Restart=always +RestartSec=5 +DynamicUser=yes +NoNewPrivileges=yes +PrivateTmp=yes +ProtectSystem=strict +ProtectHome=yes + +[Install] +WantedBy=multi-user.target diff --git a/slackware_changelog.py b/slackware_changelog.py new file mode 100755 index 0000000..39a8211 --- /dev/null +++ b/slackware_changelog.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +"""Slackware -current ChangeLog proxy for Homepage. + +Fetches the ChangeLog, parses the latest entry, and serves it as JSON for a +customapi widget plus an HTML page of the full entry. Homepage cannot fetch +plain text, cannot parse it, and would hammer the mirror on every refresh, +so this caches upstream and hands back structured fields. + +Copyright (C) 2026 Danilo M. <danix@danix.xyz> + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License version 2 as published by the +Free Software Foundation. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for details. +""" + +import html +import json +import os +import re +import time +import urllib.request +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + +URL = os.environ.get( + "CHANGELOG_URL", + "https://mirrors.slackware.com/slackware/slackware64-current/ChangeLog.txt", +) +PORT = int(os.environ.get("PORT", "8098")) +CACHE_TTL = int(os.environ.get("CACHE_TTL", "1800")) +TOP_N = int(os.environ.get("TOP_N", "8")) + +# "l/glibc-2.44-x86_64-4.txz: Rebuilt." - action lines start at column 0. +# Indented lines are notes belonging to the package above them. +PKG = re.compile(r"^(\S.*?):\s{2}(\w+)\.\s*$") +SEPARATOR = re.compile(r"^\+-+\+$") + +_cache = {"at": 0.0, "data": None, "error": None} + + +def parse(text): + """Latest entry only: date line, package lines, up to the separator.""" + lines = text.splitlines() + entry = [] + for line in lines: + if SEPARATOR.match(line): + break + entry.append(line) + while entry and not entry[-1].strip(): + entry.pop() + if not entry: + raise ValueError("no entry found in changelog") + + date, body = entry[0].strip(), entry[1:] + actions, packages = {}, [] + for line in body: + m = PKG.match(line) + if m: + name, action = m.group(1), m.group(2).lower() + actions[action] = actions.get(action, 0) + 1 + packages.append({"name": name, "action": action}) + + security = sum(1 for line in body if "(* Security fix *)" in line) + cves = sorted(set(re.findall(r"CVE-\d{4}-\d+", "\n".join(body)))) + + return { + "date": date, + "packages": len(packages), + "upgraded": actions.get("upgraded", 0), + "rebuilt": actions.get("rebuilt", 0), + "added": actions.get("added", 0), + "removed": actions.get("removed", 0), + "security": security, + # A one-line summary is what actually fits in a widget row. + "summary": ", ".join( + f"{n} {a}" for a, n in sorted(actions.items(), key=lambda kv: -kv[1]) + ) + or "no packages", + "cves": cves, + "package_list": packages, + "entry": "\n".join(entry), + } + + +def fetch(force=False): + """Cached parse. A failed refresh keeps serving the last good entry.""" + now = time.time() + if not force and _cache["data"] and now - _cache["at"] < CACHE_TTL: + return _cache["data"] + try: + with urllib.request.urlopen(URL, timeout=30) as r: + text = r.read().decode("utf-8", "replace") + data = parse(text) + data["fetched"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(now)) + _cache.update(at=now, data=data, error=None) + except Exception as exc: # noqa: BLE001 - the reason goes to the client + _cache["error"] = f"{type(exc).__name__}: {exc}" + if _cache["data"] is None: + raise + # Stale is better than blank; say so rather than pretend it is fresh. + _cache["at"] = now + out = dict(_cache["data"]) + if _cache["error"]: + out["stale"] = _cache["error"] + return out + + +def top(data, n=None): + """Flat pkg1..pkgN keys: customapi maps fixed field names, not arrays.""" + n = TOP_N if n is None else n + pkgs = data["package_list"] + out = {} + for i, p in enumerate(pkgs[:n], 1): + # Strip the series prefix and .txz; the version is the interesting part. + name = p["name"].split("/", 1)[-1].removesuffix(".txz") + out[f"pkg{i}"] = f"{name} - {p['action']}" + if len(pkgs) > n: + out[f"pkg{n + 1}"] = f"... and {len(pkgs) - n} more" + out["date"] = data["date"] + out["packages"] = len(pkgs) + if data.get("stale"): + out["stale"] = data["stale"] + return out + + +def render(data): + """The full entry, as a page worth clicking through to.""" + rows = "\n".join( + f"<tr><td>{html.escape(k)}</td><td>{html.escape(str(data[k]))}</td></tr>" + for k in ("packages", "upgraded", "rebuilt", "added", "removed", "security") + ) + return f"""<!doctype html> +<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"> +<title>Slackware -current: {html.escape(data['date'])}</title> +<style> + :root {{ color-scheme: light dark; }} + body {{ font: 14px/1.5 system-ui, sans-serif; margin: 2rem auto; max-width: 60rem; padding: 0 1rem; }} + h1 {{ font-size: 1.2rem; }} + table {{ border-collapse: collapse; margin: 1rem 0; }} + td {{ padding: .15rem 1rem .15rem 0; }} + td:first-child {{ opacity: .7; }} + pre {{ overflow-x: auto; padding: 1rem; background: #8881; border-radius: 6px; white-space: pre-wrap; }} + .stale {{ color: #c00; }} +</style> +<h1>Slackware -current — {html.escape(data['date'])}</h1> +{'<p class="stale">Upstream unreachable, showing cached entry: ' + html.escape(data['stale']) + '</p>' if data.get('stale') else ''} +<table>{rows}</table> +<pre>{html.escape(data['entry'])}</pre> +<p><a href="{html.escape(URL)}">source</a> · fetched {html.escape(data.get('fetched', '?'))}</p> +""" + + +class Handler(BaseHTTPRequestHandler): + def do_GET(self): + path = self.path.split("?")[0].rstrip("/") or "/" + try: + data = fetch(force=(path == "/refresh")) + except Exception as exc: # noqa: BLE001 + return self._send(502, "application/json", json.dumps({"error": str(exc)})) + + if path in ("/", "/latest", "/refresh"): + body = dict(data) + body.pop("package_list", None) + body.pop("entry", None) + return self._send(200, "application/json", json.dumps(body)) + if path == "/top": + n = None + q = self.path.split("?", 1) + if len(q) == 2: + m = re.search(r"n=(\d+)", q[1]) + if m: + n = max(1, min(int(m.group(1)), 50)) + return self._send(200, "application/json", json.dumps(top(data, n))) + if path == "/packages": + return self._send(200, "application/json", json.dumps(data["package_list"])) + if path == "/entry": + return self._send(200, "text/html; charset=utf-8", render(data)) + self._send(404, "application/json", json.dumps({"error": "not found"})) + + def _send(self, code, ctype, body): + raw = body.encode() + self.send_response(code) + self.send_header("Content-Type", ctype) + self.send_header("Content-Length", str(len(raw))) + self.send_header("Access-Control-Allow-Origin", "*") + self.end_headers() + self.wfile.write(raw) + + def log_message(self, *args): + pass + + +if __name__ == "__main__": + ThreadingHTTPServer(("127.0.0.1", PORT), Handler).serve_forever() diff --git a/test_changelog.py b/test_changelog.py new file mode 100644 index 0000000..b1454b8 --- /dev/null +++ b/test_changelog.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# Copyright (C) 2026 Danilo M. <danix@danix.xyz> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +"""Self-check: run `python3 test_changelog.py`. Fakes the mirror, exercises +parsing, caching and the stale fallback.""" + +import io +import urllib.request + +import slackware_changelog as c + +SAMPLE = """Mon Sep 7 22:56:39 UTC 2026 +a/util-linux-2.42.3-x86_64-1.txz: Upgraded. + This update fixes bugs and security issues. + For more information, see: + https://www.cve.org/CVERecord?id=CVE-2026-76642 + https://www.cve.org/CVERecord?id=CVE-2026-78410 + (* Security fix *) +l/cryptopp-8.9.0-x86_64-3.txz: Rebuilt. + Fixed library location. Thanks to Petri Kaukasoina. +n/foo-1.0-x86_64-1.txz: Added. +n/bar-1.0-x86_64-1.txz: Removed. ++--------------------------+ +Sun Sep 6 00:00:00 UTC 2026 +a/should-not-appear-1.0-x86_64-1.txz: Upgraded. +""" + +fail = [False] + + +def fake_urlopen(url, timeout=0): + if fail[0]: + raise OSError("mirror down") + return io.BytesIO(SAMPLE.encode()) + + +urllib.request.urlopen = fake_urlopen + +d = c.parse(SAMPLE) +assert d["date"] == "Mon Sep 7 22:56:39 UTC 2026", d["date"] +assert (d["packages"], d["upgraded"], d["rebuilt"], d["added"], d["removed"]) == (4, 1, 1, 1, 1), d +assert d["security"] == 1, d +assert d["cves"] == ["CVE-2026-76642", "CVE-2026-78410"], d["cves"] +assert "should-not-appear" not in d["entry"], "the next entry leaked in" +assert d["package_list"][0] == {"name": "a/util-linux-2.42.3-x86_64-1.txz", + "action": "upgraded"}, d["package_list"][0] +assert "1 upgraded" in d["summary"], d["summary"] + +# An indented note that ends in a word plus a period is not a package line. +assert all("Thanks to" not in p["name"] for p in d["package_list"]), d["package_list"] + +# Cache holds, and a failing refresh keeps serving the last good entry. +first = c.fetch() +assert first["date"] == d["date"] +fail[0] = True +stale = c.fetch(force=True) +assert stale["date"] == d["date"], "lost the cached entry on a failed refresh" +assert "mirror down" in stale["stale"], stale.get("stale") + +# /top: flat keys, prefix and .txz stripped, overflow line when truncated. +t = c.top(d, n=2) +assert t["pkg1"] == "util-linux-2.42.3-x86_64-1 - upgraded", t["pkg1"] +assert t["pkg3"] == "... and 2 more", t +assert t["packages"] == 4 and t["date"] == d["date"], t +assert "pkg4" not in t, t +assert "pkg5" not in c.top(d, n=10), "overflow line on a complete list" +assert c.top(d, n=10)["pkg4"] == "bar-1.0-x86_64-1 - removed", c.top(d, n=10) + +page = c.render(c._cache["data"]) +assert "Sep 7" in page and "util-linux" in page +assert "<script" not in page.lower(), "changelog text must not reach the page as markup" + +print("ok") |
