Skip to content

Releases

v0.8.2

Choose a tag to compare

@vidit-admin vidit-admin released this 30 Oct 17:30
7c8edb7

Fixed

  • While a GIF splits into its frames, the proof image block or the source file slot that picked it shows Splitting villaggio.gif into 3 frames…, then says how many images it became (#819).

v0.8.1

Choose a tag to compare

@vidit-admin vidit-admin released this 29 Oct 12:56
154f6c0

Added

  • GET /feed takes the date, media, author and location guess filters, GET /search takes has_coords and GET /events takes media; GET /search/dates and GET /feed/dates count the matching events per day (#813).
  • GET /sites, an event's site links and the site suggestions carry each site's picture in media, the image of its first source in source order that is not graphic; a site search hit carries the same picture (#813).
  • GET /search/sections returns the top hits of each kind in one request, each section the first page of that kind's list with its cursor (#813).
  • Search finds sites by their name or by the title or note of one of their sources, and the search page's Show field picks every kind: Requested, Detected, Geolocated, Sites, Collections, Analysts and Organizations (#807).
  • On the event forms, each source carries Move earlier and Move later, and readers see the sources in the order the author set (#804).
  • An admin can restore a soft-deleted account from the onboarding table: the account and the events its soft delete hid come back (#793).
  • On the search page, a typed coordinate pair offers to open that point on the map, and on All a query that names an analyst or an organization exactly puts a link to their page right under the search field (#791).
  • When a search finds nothing, the search page suggests the closest spelling that finds results, such as Khan Yunis for Khan Younis or a handle missing a letter (#790).
  • A map control beside the zoom buttons picks the basemap: Auto, which follows the theme, Dark, Light, Streets or Terrain; the browser keeps the choice (#709).
  • On the Dark and Light basemaps, the same control sets the label language (Latin, local or both), the label density, and whether region borders and roads show (#709).
  • The basemap control turns building footprints on or off on the Dark and Light basemaps (#709).
  • The Dark and Light basemaps can highlight military areas and airports, with their runways, from zoom 10; the highlight is off by default and the basemap control turns it on (#709).
  • A coordinate grid overlay, off by default, draws latitude and longitude lines with their labels on every basemap, finer as you zoom in (#709).
  • The Dark and Light basemaps draw disputed country borders dashed, and the basemap control can draw them solid (#709).
  • A Relief switch on the Dark and Light basemaps shades hills and mountains from open elevation data; it is off by default (#709).
  • A Satellite basemap draws the cloud-free Sentinel-2 mosaic of 2016 by EOX (#709).
  • Two map overlays, off by default, show NASA data of the day before on every basemap: true-colour satellite imagery and fire detections (#709).
  • An instance can serve the basemap tiles, icons and relief from its own S3-compatible bucket: the backend's update-map command copies them there on a schedule, and NEXT_PUBLIC_MAP_ASSETS_URL points the map at them (#709).
  • You can attach a GIF as a source file or a proof image: each of its frames is stored as a PNG image of its own, up to 6 frames; a longer GIF is refused with a request for a video or fewer frames (#744).
  • Type a latitude and a longitude, or paste a Google Maps link, to centre the map there: from a map control beside the basemap control on the map, and above the contour map of the site forms; a pair out of bounds says why (#748).
  • The tag field of the event and source forms suggests your most used tags, or the platform's, in one line of one-click chips under the search; GET /tags/suggested lists them (#725).
  • When a detection's proof holds the footage its source lacks, the edit form copies that proof image into the empty source file slot in one click; POST /uploads/from-proof stages the copy (#746).
  • A pulsing dot above the basemap control shows while the daily satellite imagery or the fire detections are still loading, including after you move the map (#758).
  • In the basemap control, a ? beside each basemap, label setting, layer and overlay says what it shows and links to the source of its data or tiles (#757).
  • When the basemap's tile server does not respond, offline or with one provider down, a line over the map says so instead of leaving it blank (#792).
  • Saving a site edit asks for an optional version note, like an event edit, and the site's history shows it (#800).

Changed

  • /palette shows High level and Low level primitives from the import graph, the filter panel as one demo, and its check fails on a missing entry, a wrong tier or a primitive only one component uses (#814).
  • docs/design.md names one primitive per job (show an item, pick from a vocabulary, enter a date, remove, delete, show an error...), and lint fails on a raw <select>, <textarea> or date input, confirm() or a retired primitive (#814).
  • /palette lists the shared components by category, one file each, with a table of contents and a filter by name; each entry shows its live demos only, and docs/design.md holds the details (#814).
  • On the event and site forms, the coordinates field sits under the map, the title fields drop their character counter, and the suggested tags show as many as fit on two lines with no Show more (#814).
  • The Filters header and the phone's filter bar show how many results match, such as 284 results, on the map, search, the feed and the Requests board; GET /search/dates and GET /feed/dates return that total (#814).
  • A removal uses the cross and acts at once; a deletion or a revocation uses the trash and confirms in place: revoking an invite code shows the trash instead of a ban sign (#814).
  • GET /feed and GET /feed/dates take the Show filter as status (geolocated, requested, detected), the same parameter and values as GET /search and GET /events, and no longer accept kind (#814).
  • The Requests board keeps its filters in the page URL, like the map, search and the feed (#814).
  • Every field that picks from a list, in the filter panel and on the forms (kinds, levels, reasons, effects, conflicts, tags), lists its options on focus, closes after a pick and shows the picks as removable pills (#814).
  • Every field that finds an event, a site, a collection, an analyst or an organization lists its matches the same way, and Find a collection now runs the collection's own action on a pick (#814).
  • One filter panel serves the map, search, the feed and the Requests board: collapsible groups What, When (the Event date and Added timelines), Source, Where and Who; Conflict, Tags and accounts search in their line; a phone opens it in a sheet (#813).
  • Search, the feed and the Requests board show the filter panel under the map's Filters header, open by default and closed on the map; each surface remembers your choice, and the map header drops its point count (#813).
  • The feed keeps its filters in the URL, and its Following choice is a switch in the filter panel; search lists an analyst or an organization the query names once, first in its section (#813).
  • Every list shows an event, a request, a site or a collection one way, as a compact row, a list row or a feed card: its picture or its kind's icon, the title, a line with the kind, date, analyst and place, then its state (#813).
  • Breaking: GET /search returns one ranked list, hits, with one cursor that pages it, in place of five groups with their totals; type repeats to pick the kinds, and status narrows only the events (#807).
  • The search page lists the top hits of each kind under its name, and See all opens one kind's list with Show more; the URL keeps the query and the kind, so Back and shared links work. The profile's links into search read See all (#813).
  • The Links help on the source forms holds two sentences, and the methodology guide says how to link, archive and permalink a source (#804).
  • The suggested tags under a tag field wrap instead of running off the card, the first 6 then one Show more (#804).
  • A minor destructive action, such as dismissing a notification...
Read more

v0.8.0

Choose a tag to compare

@vidit-admin vidit-admin released this 25 Oct 20:45
9bf45ba

Added

  • An event holds up to 10 sources, each with a level (direct, indirect or context), a kind (such as drone, aircraft or satellite), a capture time and position, a graphic flag, tags, a file, and up to 10 links with their post time and archived copy (#693).
  • POST /uploads stages one source file before the event write that uses it; a staged file nobody uses is deleted after 24 hours (#693).
  • Search matches source titles and source URLs, and the map, list, search and feed filters take source_kind and level (#693).
  • The event page and both event forms show the sources side by side in one row with arrows, each reading title, level, kind, links, capture time, capture position, then tags; a lone source shows its file beside them on a wide screen (#693).
  • In the event form, each source link is one row with its post time and archive box, the capture details fold under More details, removing a source takes a second click, and a publish attempt shows the first source with a missing field (#693).
  • A satellite or aircraft source warns, without blocking, when no imagery provider lets a reader reopen the image at its date; the form names the provider, how to reopen the image and its credit, and POST /sources/check runs the check (#693).
  • Submitting a geolocation or publishing a request or a detection invites the co-geolocators you pick by the byline, in the same write; POST /events and POST /events/{id}/versions take geolocator_invites (#693).
  • The feed shows machine detections from the analysts you follow, or from everyone on All, and a Show detections switch in its Kind filter hides them; GET /feed takes kind=detected (#693).

Changed

  • Every event write takes a sources list in place of the source URL, mirror, snapshot, capture position, post time, graphic flag and file fields, and event reads serve sources in place of the source and archive fields (#693).
  • Publishing needs a direct or indirect source with a file and a conflict; no tag is required. Every source needs a kind, unknown included. A request needs a direct or indirect source with a file (#693).
  • The analyst who publishes a request can correct its sources, and batch completion takes a source kind per detection in place of a capture-source tag (#693).
  • The graphic flag belongs to each source, and an event is graphic when any of its sources is; the admin moderation flag sets or clears it on every source (#693).
  • A request or a detection is version 0, published from its edit form through POST /events/{id}/versions; the published event is version 1, and the history and the version selector name version 0 Request or Detection (#693).
  • POST /events/{id}/versions takes an optional version_no, the version the form was loaded at, and answers 409 version_mismatch when the event has moved past it (#693).
  • A re-import or a bot re-tag never writes over an event that has a saved version (#693).
  • Profile stats count events per source kind, the source hosts read each event's first source URL, and GET /tags lists the tags on live events or their sources (#693).
  • API and database fields follow one naming convention: events use occurred_date, occurred_time and coords, media use kind, conflicts use is_ongoing, and foreign keys end in _id. The old field names no longer work, with no alias (#693).
  • An organization invitation is withdrawn by the invitee's username instead of an invitation id, and the organization and event invitation lists report invited_at in place of created_at (#693).
  • Approvals are final: Approve asks you to confirm first, joining an event as a co-geolocator deletes your approvals of it, and a site admin can delete an approval, after which its reviewer or organization can approve that version again (#693).
  • Approvals show under the event actions as the reviewers' pictures with a check badge, and a geolocator asks for one from a Request approval action (#693).
  • Notification settings are one email switch per kind, and the in-app notification list always shows every kind (#693).
  • A site admin links an X handle to an account (#693).
  • The worker runs the hourly bot mentions catch-up when it has the bot's X read credentials, so the backend image has no bot command and the bot needs no schedule of its own. Mentions queued by the webhook and the catch-up share one table (#693).
  • To upgrade, stop the worker, then run the database migrations, which move every event onto sources and lose data on downgrade. Give the worker the bot's X read credentials, and call reap-password-reset-tokens in place of reap-auth-tokens (#693).
  • The event page and both event forms share one layout: the title, which a form's Title field replaces, and the geolocators, the actions pinned top right over the approvals, then sources and proof beside location and Details (#693).
  • Submit opens on three cards, Single event, From an X post and Bulk import, in place of the mode switch; the chosen path stays in the address, and the back arrow returns to the cards (#693).
  • Both event forms pin their buttons on one line with a count of what each publish still misses, ask for a second click on every publish, and check a write in the same order with the same messages (#693).
  • A submit with blank required fields turns them red in place and reads the list to a screen reader, with no box listing them (#693).
  • Saving a version asks for its optional note in a small panel on the Save button, and the owner closes the event from a red trash button beside it (#693).
  • Latitude and Longitude sit on one line at every width, followed by the map and copy actions as separate buttons, in the Location card and in each source's capture position (#693).
  • Tags and conflicts are picked through a search field that suggests matches as you type, in the event form, each source, and the filters of the map, search, the Requests board and the feed; the form offers to create a missing tag (#693).
  • A remove control that asks before it removes shows a red trash, and one that removes at once shows a red cross, on the event forms and the approval stamps (#693).
  • The author filter shows among the active filter pills, as By username, so one click removes it like any other filter (#693).
  • On an event page, Add to collection and Geolocators open their panel over the page, anchored to the button, so nothing on the page moves (#693).
  • The event title, source titles, the version note, the close reason and report details count the characters used against their limit; link fields stop at the API's cap, and an out-of-range capture position is flagged (#693).
  • A write drops the capture position of an article, an official statement or an other source, since a document has none (#693).
  • Deleting a collection that an open report names withholds it from everyone at once, keeps it in the admin reports queue marked Deleted by its owner, and erases it when its last open report is resolved (#693).
  • The collection forms and the profile and organization edit modes pin Save and Cancel at the top right, and a collection's form flags its missing fields and adds events from a search list (#693).
  • A report opens in a dialog, and removing or leaving a geolocator takes a second click (#693).
  • Sign-in, registration, password reset, Change password and the invite form outline every blank required field at once and read the list to a screen reader, and the Settings accent colour picker moves between swatches with the arrow keys (#693).
  • The admin console finds analysts and events by name, confirms every destructive action in a dialog (revoking an invite code included), groups row actions in a menu, and pages the reports queue with Load more (#693).
  • GET /admin/me answers every signed-in analyst with their is_admin value instead of a 403 for an analyst who is not an admin (#693).

Removed

  • Push notifications: email and the in-app list are the two ways to get a notification, and the push settings, devices and keys are gone (#693).
    -...
Read more

v0.7.4

Choose a tag to compare

@vidit-admin vidit-admin released this 22 Oct 05:16
d4e129f

Changed

  • The backend image runs each role with one command, api, migrate, worker, bot or sync-conflicts, so a host only chooses which command to run and when (#659).
  • The landing roadmap lists the planned outcomes with an In progress or Planned status instead of versions and links to the changelog for what shipped, and the contributor docs point to the issues and their epics for planning (#635).
  • A frontend build loads media images from a CDN only when NEXT_PUBLIC_MEDIA_HOST names it, and the config examples, comments and docs describe what any instance sets instead of one instance's hosting (#661).

Removed

  • The repository no longer carries the vidit.app deploy workflow, its hosting config, the backup job or make import-prod; the docs describe what any instance runs: the backend roles, their schedules, backups and restores (#660).
  • The promo video and guide screenshot tooling is maintained separately and no longer ships in this repository; the guide screenshots stay (#648).

Security

  • The backend image no longer copies local .env files, virtual environments, local media or datasets from the build directory (#661).
  • The frontend test tooling runs on Vite 8.3.5, which fixes advisories that let a page or a crafted path read files or run commands through a running Vite dev server (#654).
  • The backend decodes uploaded images with Pillow 13.0.0, which fixes a heap buffer overflow in its JPEG decoder that a crafted upload could trigger (#653).

v0.7.3

Choose a tag to compare

@vidit-admin vidit-admin released this 21 Oct 17:17
46fa08a

Security

  • The backend no longer trusts the emSign ECC Root CA - G3, whose eight eMudhra intermediates were compromised in October 2026, so a certificate forged with those keys cannot intercept its calls to email, storage, X or Sentry (#647).

v0.7.2

Choose a tag to compare

@vidit-admin vidit-admin released this 19 Oct 04:59
119af42

Changed

  • The admin console no longer has the catalogue feed or maintenance panels, and one "Manage an event" panel replaces the event moderation and event delete panels (#617).

Fixed

  • The archive import page asks you to confirm before you close or reload it while your archive is still being prepared or uploaded, since leaving then loses the upload (#618).

v0.7.1

Choose a tag to compare

@vidit-admin vidit-admin released this 18 Oct 13:10
4fd99cb

Added

  • From Settings, you can mint up to five invite codes a day, each bound to the invitee's X handle, and revoke an unused one; admins see who minted each code and can turn minting off for an analyst (#609).
  • The Getting started page shows a new analyst around Vidit in six linkable sections of annotated screenshots and icons; the registration success screen and the confirmation email lead to it (#611).

Changed

  • The test CI finishes in under 2 minutes instead of 4: the backend suite runs on three runners, the frontend checks run as parallel jobs, and the narrow-viewport smoke tests run against a production build with two workers (#613).
  • The rail shows Requests only to a signed-in reader; a request page still opens from a shared link (#611).
  • The import guide numbers the parts of a sample post instead of listing them, the methodology guide opens on a real annotated proof, and each guide states a fact once and links the others (#611).
  • The landing roadmap lists v0.8 cleanup and restructuring, v0.9 moderation, v0.10 search and discovery, and a v1.0 that opens reader accounts while publishing stays with contributors (#607).

Fixed

  • The import completion email tells you to use Start reviewing and go through detections one by one, instead of describing a page-at-a-time publish the queue no longer offers (#612).
  • The account picker's suggestions show in full wherever the picker sits, such as the Request approval field in an event's Approvals card, and open above the field near the bottom of the screen (#602).

v0.6.7

Choose a tag to compare

@vidit-admin vidit-admin released this 11 Oct 15:37
9a5d560

Changed

  • Proofs and collection descriptions use a block editor with a slash menu and are stored as sanitized HTML; existing bodies convert automatically, dropping list start numbers, code block languages and image titles (#542).

v0.6.4

Choose a tag to compare

@vidit-admin vidit-admin released this 15 Sep 20:27
439b811

Added

  • Collections: two tables, eight routes, and the takedown (backend/app/models/collection.py, backend/app/services/collections.py, backend/app/routers/collections.py, backend/app/services/event_filters.py, docs/api.md, docs/data-model.md) (#343). An analyst's named, curated set of their own geolocated and detected events: collections and collection_events, both foreign keys cascading so a GDPR erasure passes through. A collection carries two required free-text fields, a title capped like an event's and a short description saying what it holds, capped at 500 characters in schemas/collection on the profile bio's figure. One validator covers the pair on the create and the update alike, so both are stripped of surrounding whitespace and a value of spaces is a 422 rather than a stored blank. Create, read, write both details in one PATCH, drop, page the items, put one on or take one off, plus the two list reads on GET /users/{username}/collections and GET /events/{id}/collections. Every write is owner-only, and an event joins its owner's collection alone, the one invariant the service carries rather than a SQL constraint. What a collection may show is one predicate, collectable_events, read by the item page, the count, the date range, the card mosaic and the add verb, so a row that closes or is taken down leaves all five at once with no write to the membership table. A collection stores no picture: the cover on the read is up to four tiles taken off the items themselves in chronological order, graphic rows skipped and an image preferred over a clip, ranked for a whole page of collections in one windowed query. The items page on a cursor keyed on event_date, event_time, created_at, id, undated last. PATCH /admin/collections/{id}/moderation moves a reported collection along the reversible hidden_at an event already carries, withholding it with {"hidden": true} and restoring it with {"hidden": false}, audited as collection_hidden and collection_restored; DELETE /admin/collections/{id} stays as the takedown alias, and a withheld collection 404s for everyone but an admin.
  • Collections on the front end: the collection page, the profile section and the add-to-collection panel (frontend/src/lib/collections.ts, frontend/src/app/collections/[id]/page.tsx, frontend/src/components/collections/, frontend/src/components/map/CoverageMap.tsx, frontend/src/app/profile/[username]/page.tsx, frontend/src/proxy.ts, docs/design.md) (#343). Three surfaces read or write one. /collections/{id} opens on the title, the owner's byline, the meta line stating the count and the span, and the description under it at reading size, then the items on the shared Coverage map and the chronological list under it, both off one set so the pins and the rows cannot describe different collections. The profile card wears a mosaic of the collection's own first items, the way a playlist icon is made of the covers of what is on it: one tile fills the 16:9 slot, two split it, three stand the earliest tall on the left, four fill a 2x2, and a collection with nothing to show falls back to the site's one no-media box. Nothing is uploaded and nothing is stored, so a card cannot fall out of step with its shelf, and the page header carries no picture at all. The profile gains a Collections grid between Insights and Recent submissions, four cards with a Show more that opens the analyst's whole shelf in /search on type=collection&author=, the submissions list's link through the same builder; the grid is absent for a visitor with nothing to show and a first-run surface for its owner. GET /search browses the collections group on author alone, so an empty query lists that analyst's visible, non-empty collections newest first. Every card carries the description clamped to two lines. The three writes that take a collection's details are one form, the title over the description with the shared remaining / cap counter on each and a submit that refuses a blank or over-long value on either. The owner's two verbs ride the header cluster with their panels under it, the split the event page uses, and only dropping one is red, behind the two-click confirm, since the events it held stay as they were. An event's owner shelves it from a panel in the header's management tier whose rows toggle optimistically and roll back on a refusal. /collections joins the public prefixes in proxy.ts, so a shared link reads signed out like the events it holds, and the narrow-viewport suite gains the page as its owner at both phone widths.
  • Collections are a search result group (backend/app/services/search.py, backend/app/schemas/search.py, backend/app/routers/search.py, backend/app/services/event_filters.py, backend/alembic/versions/q5s7u9w1y3a5_collections.py, frontend/src/app/search/page.tsx, frontend/src/components/collections/CollectionCard.tsx, docs/api.md, docs/design.md) (#343). GET /search answers with a fourth group. A query matches a collection's title and its description as one document, under a GIN index on the same simple configuration the events and users groups take, ranked by ts_rank with created_at descending as the tie-break. type=collection asks for the group alone and type=all carries it. A hit is the collection itself, the one CollectionRead every read surface renders, so a result opens on the same mosaic, count and date range the profile prints; a withheld collection, one whose owner is soft-deleted and one holding nothing showable are all absent, the terms a visitor already reads a profile on. The event filters empty the group, being predicates on an event, with one exception: author names an analyst, which a collection carries, so it narrows the group to that owner's shelves. The search page gains a Collections chip between Events and Analysts and renders each hit as the profile's own card, with the byline that card hides on a profile.
  • Step through a collection on its own page, and a Description card for its description (frontend/src/app/collections/[id]/page.tsx, frontend/src/components/collections/CollectionReader.tsx, frontend/src/components/collections/ReaderHeader.tsx, frontend/src/components/collections/CollectionItems.tsx, frontend/src/components/map/DetailSidePanel.tsx, frontend/src/components/map/Map.tsx, frontend/src/components/ui/EntityCard.tsx, docs/design.md) (#343). The collection page reads as three sections under its header: Description, which prints the description whole at reading size where it used to crowd the header; Coverage, now a player; and Events. The player is the map page's own two surfaces rearranged around a position, the shared map over every item's pin with the current one lit and the walked ones dimmed, and that item's event in the map's own detail panel beside it, under a header saying N of M with the two moves. The panel gains an inline placement for it, a column of the page with no viewport insets, no bottom sheet and no height cap of its own, so the two sit side by side in a 32rem block from sm up and stack below it with the step controls at the panel's top edge on the app's phone tap step. Nothing joins the pins: the order is said by the counter and by which pin is lit. The step rides the page's own URL as ?step=, 1-based and clamped, moved with router.replace so the back button leaves the page rather than walking back through every step, and ArrowLeft / ArrowRight step too unless a field has the caret. A click anywhere on a row of the Events list moves the player to it and lights that row, the new onSelect / selected pair on the catalogue card, which keeps the way to the event's own page on the row's title. One cursor walk serves the pins, the panel and the rows, so N of M counts what the list shows. The narrow-viewport suite gains the player as a visitor at both phone widths.
  • Opening and changing a collection are pages of their own, and the profile card drops its type square (frontend/src/app/collections/new/page.tsx, frontend/src/app/collections/[id]/edit/page.tsx, frontend/src/app/collections/[id]/page.tsx, frontend/src/components/collections/, frontend/src/lib/collections.ts, docs/design.md) (#343). /collections/new takes the title and the description behind the wall, the way /submit takes an event, and /collections/{id}/edit takes the same two for a collection its owner already holds. Both are the one detai...
Read more

v0.6.3

Choose a tag to compare

@vidit-admin vidit-admin released this 15 Sep 19:49
914da38

Added

  • @ViditBot opens a request for a coordinate-less mirror post (backend/app/services/tweet_ingest/resolve.py, backend/app/services/tweet_ingest/urls.py, backend/app/services/detection.py, backend/app/services/events.py, backend/app/services/bot.py, backend/app/models/bot_mention.py, frontend/src/app/import/page.tsx, docs/ingestion.md, docs/data-model.md) (#342). Analysts who mirror a Telegram channel or another account's post, re-uploading the clip and linking the original, got a flat coords_missing refusal: the post carried everything a request needs except the geolocation, which is the one thing a request asks for. The engine now emits a RequestDraft beside that refusal, read off the same source, media, title and date derivations the detections use, when the thread carries footage, it resolved a source, and no coordinate appears anywhere in the thread: a coordinate, own post or quoted post, is a geolocation, never a request. The source's host decides what gets fetched, never whether a request opens: an X status and a public t.me post are chased, so the original's clip and post date fill the row, and a link on any other host, a YouTube video or a news page, opens a request over the analyst's own attached clip with the source's date left unknown. The bot writes it through detection.open_request, which calls the same events.create_request a person's request goes through: the row is owned by and credited to the tagging analyst, stamped requested_at, carrying the post it points at as its source_url and the thread's footage as its one role=source media, with no coordinate. It also carries the same five provenance columns a detection carries, detected_via reading bot, stamped by one shared events.stamp_provenance. A coordinate-less re-tag, of the same post or of a repost of it, lands on that row and moves nothing, so the bot stays silent; a tag carrying the coordinate the analyst has since worked out is a geolocation, so it lands a detected row beside the open request, which stays its owner's to withdraw. A soft-deleted row never matches, so a takedown does not fence the owner off from mirroring that footage again. An X status and a t.me post are stored in one canonical spelling, so two spellings of one of them open one request; every other host is stored as the post wrote it. The footage is an ordered candidate list, the source's media then the analyst's own video, and the first that fetches fills the slot; a chase that failed transiently drafts nothing at all, so the next tag retries it. A clip the evidence intake refuses earns a new footage_unusable refusal in the reply rather than one naming a missing coordinate, and a mirror the branch can never serve, one pointing at a source with no footage to store, earns a new request_not_possible one telling the analyst to attach the clip. The coordinate-less shapes a re-tag or a rewrite still answers keep coords_missing, and so do the paste and the archive, which never ask for a request. The tag earns a linkless ✅ Geolocation request opened · ref … reply under the same per-hour and per-author budget every other reply spends, and the mention is ledgered requested. Nothing else moves: the refusal still travels with the draft, so the paste and the archive, which read detections alone, answer exactly what they did. The reply carries the two warnings a request can earn, source_date_unknown and duplicate_media, read off the same _write_warnings pass the detections run; a request is always born with footage, the analyst's own copy filling the slot when the chase served nothing, so source_fetch_failed never reaches one. The admin reject rate (backend/app/services/admin.py) now reads one Event.is_machine_detection predicate on the model, an imported row that is not a request (requested_at NULL), so a bot-opened request never enters the cohort; the admin panel copy and the event page's provenance row, which reads Requested from on a request, say the same. No migration, since bot_mentions.outcome carries no database domain.
  • An open request is edited by its owner, and listed on their profile (backend/app/services/events.py, backend/app/routers/events/item.py, frontend/src/app/events/[id]/edit/page.tsx, frontend/src/components/geolocations/edit/RequestEditForm.tsx, frontend/src/components/event/useEventActions.tsx, frontend/src/components/profile/OpenRequests.tsx, frontend/src/lib/events.ts, docs/api.md, docs/data-model.md, docs/ingestion.md) (#342). The bot opens requests on the analyst's behalf and answers Edit it from your profile, and neither half of that was reachable: the owner of a requested row could view it, answer it through the submit form or close it, and the edit address turned every other status away, while the profile listed published work and machine detections and nothing else. POST /events/{id}/request is the owner's correction of an open request, owner-only and requested-only, taking the create form's fields whole on the same rules: the coordinate guess and the camera point stay optional and both-or-neither, the curated floor stays unenforced until the geolocate, the mirrors and their archived copies replace what the row held, and the source media moves on the remove_media_ids + files pair the published writes take, under the same one-source cap and the same source floor. No version is filed: a version supersedes a vouched claim and a request is a question, so the row is overwritten in place and keeps its id, its requested_at, its requester and its provenance columns. source_posted_at is optional on this path alone, since the bot opens requests whose source date it could not read and the owner must not have to invent one. The write takes the same row lock the geolocate, the version and the close take, so an edit racing a fulfilment or a withdrawal serializes with it and the loser sees the 409. The edit address now serves three shapes, a request being the third, and the request page and the event page carry the owner's pencil beside the close. Open requests joins the profile under the submissions, built from the same compact cards, reading the public requested view filtered to that analyst and expanding into search. The POST /events/requests reference rows that claimed the proof body was image-free and that event_time needed event_date are corrected to what the endpoint does.

Changed

  • The engine's warning and refusal sentences say what to do next (backend/app/services/tweet_ingest/resolve.py, backend/app/services/bot.py, frontend/src/app/import/page.tsx, docs/ingestion.md) (#342). One table words every code for the three surfaces that show it, the bot's reply, the archive's outcome email and the paste's response, so the rewording reaches all three at once: source_missing names the link that is absent rather than the search that failed, the two footage codes say which half came back empty, source_date_unknown and duplicate_media read as the row's state, and coords_invalid gives the bounds a coordinate has to sit in. The bot's request reply opens ✅ Geolocation request opened rather than restating what it did not find.

Fixed

  • A reply that only inherits the tag from its parent is not a mention (backend/app/services/tweet_ingest/extract.py, backend/app/services/bot.py, backend/app/services/x_api.py, backend/app/routers/webhooks.py, backend/app/models/bot_mention.py, frontend/src/app/import/page.tsx, docs/ingestion.md, docs/data-model.md) (#342). X opens a reply with the parent's author and the parent's mentions minus the replier, so an analyst answering a colleague whose post tagged the bot mentioned the bot without typing a character. That reached the bot on both deliveries, and since the thread under it is the replier's own and carries no coordinate, a linked analyst got a ❌ No coordinate in the post on somebody else's thread for a tag nobody made. A mention now counts as a tag only where its author typed it: on a post that is not a reply every @ViditBot is typed, and on a reply one sitting past the leading run of mentions is, the dot-mention .@ViditBot included, since the period is a character X never writes into a prefix. A reply whose only @ViditBot sits inside that run is settled by its parent, read once through syndication, the free path: a parent that mentions the bot, the bot's own post included, is where the tag came from, so nothing is acquired, nothing is answered and the mention is ledgered inherited. A parent that mentions the bot nowhere leaves the tag typed, so the bare @ViditBot under the analyst's own coordinate post and the climb it opens are untouched, and a p...
Read more