Update (da testare) x salvare task Run/Done
This commit is contained in:
@@ -302,15 +302,17 @@ namespace EgwCoreLib.Lux.Data.Services
|
||||
/// </summary>
|
||||
/// <param name="ruid">RUID richiesta</param>
|
||||
/// <returns></returns>
|
||||
public async Task CompleteRequestAsync(string ruid)
|
||||
public async Task<string> CompleteRequestAsync(string ruid)
|
||||
{
|
||||
var hashKey = GetRequestKey(ruid);
|
||||
string retVal = $"{hashKey}";
|
||||
DateTime adesso = DateTime.Now;
|
||||
var processEnd = DateTimeOffset.Now.ToUnixTimeMilliseconds();
|
||||
|
||||
// Recupero processStart e altri dati
|
||||
var processStartValue = await _db.HashGetAsync(hashKey, "processStart");
|
||||
if (processStartValue.IsNull) return;
|
||||
if (processStartValue.IsNull)
|
||||
return retVal;
|
||||
var envir = await _db.HashGetAsync(hashKey, "envir");
|
||||
var tipo = await _db.HashGetAsync(hashKey, "tipo");
|
||||
|
||||
@@ -400,6 +402,9 @@ namespace EgwCoreLib.Lux.Data.Services
|
||||
|
||||
// Attendo completamento di tutti i task
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
// restituisce hashKey trovata
|
||||
return retVal;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -322,7 +322,7 @@ namespace EgwCoreLib.Lux.Data.Services
|
||||
/// <param name="uid"></param>
|
||||
/// <param name="env"></param>
|
||||
/// <param name="estimContent"></param>
|
||||
public async Task<bool> SaveProdEstimationAsync(string uid, EgwMultiEngineManager.Data.Constants.EXECENVIRONMENTS env, string estimContent)
|
||||
public async Task<bool> SaveProdEstimateAsync(string uid, EgwMultiEngineManager.Data.Constants.EXECENVIRONMENTS env, string estimContent)
|
||||
{
|
||||
// salvo in cache
|
||||
string currKey = $"{redisBaseKey}:{env}:Estimation:{uid}";
|
||||
|
||||
@@ -5,7 +5,10 @@ using Microsoft.Extensions.Configuration;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
using StackExchange.Redis;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using static Egw.Window.Data.Enums;
|
||||
|
||||
namespace EgwCoreLib.Lux.Data.Services
|
||||
{
|
||||
@@ -17,6 +20,7 @@ namespace EgwCoreLib.Lux.Data.Services
|
||||
{
|
||||
// conf redis service
|
||||
_redisService = redisService;
|
||||
_db = redisConn.GetDatabase();
|
||||
chPub = _config.GetValue<string>("ServerConf:ChannelPub") ?? "";
|
||||
queueCalcKey = (RedisKey)$"{redisBaseKey}:CalcQueue";
|
||||
queueDoneKey = (RedisKey)$"{queueCalcKey}:Done";
|
||||
@@ -28,6 +32,7 @@ namespace EgwCoreLib.Lux.Data.Services
|
||||
Log.Info($"ProdService | Init OK");
|
||||
}
|
||||
|
||||
private readonly IDatabase _db;
|
||||
#endregion Public Constructors
|
||||
|
||||
#region Public Enums
|
||||
@@ -131,6 +136,61 @@ namespace EgwCoreLib.Lux.Data.Services
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifica un job in una coda, se trovato lo "avanza" a fatto (per estimation/balance, ...)
|
||||
/// </summary>
|
||||
/// <param name="key2adv">chiave da cercare x avanzarla</param>
|
||||
/// <param name="qType">coda di default a run --> done</param>
|
||||
/// <returns></returns>
|
||||
public async Task<string> SetDone(string key2adv, QueueType qType = QueueType.running)
|
||||
{
|
||||
Stopwatch sw = new Stopwatch();
|
||||
sw.Start();
|
||||
string reqUid = "";
|
||||
RedisKey rKey = qKey(qType);
|
||||
// LPOS restituisce l'indice del valore nella lista
|
||||
long? index = await _db.ListPositionAsync(rKey, "valore_da_cercare");
|
||||
|
||||
if (index.HasValue)
|
||||
{
|
||||
// Il valore esiste alla posizione index
|
||||
var valore = await _db.ListGetByIndexAsync(rKey, index.Value);
|
||||
reqUid = $"{valore}";
|
||||
// Il secondo parametro indica quante istanze eliminare: 0 = tutte, 1 = la prima dall'inizio, -1 = l'ultima dalla fine
|
||||
long rimosse = await _db.ListRemoveAsync(rKey, "valore_da_cercare", 0);
|
||||
// se tolte
|
||||
if (rimosse > 0)
|
||||
{
|
||||
// inserisco in coda nuova!
|
||||
_redisService.QueuePush(qKey(qType + 1), (RedisValue)reqUid);
|
||||
}
|
||||
}
|
||||
#if false
|
||||
// prendo dalla coda primo job (rimuovendolo...)
|
||||
var rawReq = await _redisService.QueuePopAsync(rKey);
|
||||
if (rawReq.HasValue)
|
||||
{
|
||||
string reqUid = $"{rawReq}";
|
||||
// metto UID in coda running nella successiva.. se non ultima...
|
||||
if (qType < QueueType.running)
|
||||
{
|
||||
// FixMe ToDo !!!: salvataggio data-ora per indicare avvio calcolo...
|
||||
_redisService.QueuePush(qKey(qType + 1), (RedisValue)reqUid);
|
||||
}
|
||||
// recupero richiesta serializzata
|
||||
string currKey = $"{redisOrderReqKey}:{reqUid.Replace("/", ":")}";
|
||||
var rawRes = await _redisService.GetAsync(currKey);
|
||||
if (!string.IsNullOrEmpty(rawRes))
|
||||
{
|
||||
result = rawRes;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
sw.Stop();
|
||||
Log.Info($"SetDone | queue: {rKey} | reqUid: {reqUid} | {sw.Elapsed.TotalMilliseconds:N3} ms");
|
||||
return reqUid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Numero di Job in attesa su coda
|
||||
/// </summary>
|
||||
@@ -318,6 +378,62 @@ namespace EgwCoreLib.Lux.Data.Services
|
||||
return rKey;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Esegue avanzamento nella coda logica tra wait/run/done se possibile...
|
||||
/// </summary>
|
||||
/// <param name="hashKey">Chiave hash del contenuto richiesta RUID</param>
|
||||
/// <returns></returns>
|
||||
public async Task UpdateQueue(string hashKey)
|
||||
{
|
||||
// recupero info accessorie da redis
|
||||
var tipo = await _db.HashGetAsync(hashKey, "tipo");
|
||||
|
||||
// verifico SE si tratta di un create/estimate e nel caso processo coda req x sistemare
|
||||
if (!tipo.IsNull && !string.IsNullOrEmpty(tipo))
|
||||
{
|
||||
string rKey = "";
|
||||
var UID = await _db.HashGetAsync(hashKey, "UID");
|
||||
var parti = ((string)tipo).Split('-');
|
||||
if (parti.Length == 2)
|
||||
{
|
||||
if (parti[0] == "ORDER")
|
||||
{
|
||||
if (parti.Length == 2)
|
||||
{
|
||||
rKey = $"{parti[0]}:{parti[1]}:{UID}";
|
||||
}
|
||||
}
|
||||
// cerco solo tipo create / 5...
|
||||
else if (parti[0] == "5")
|
||||
{
|
||||
if (int.TryParse(parti[0], out int iMode) && int.TryParse(parti[1], out int iSubMode))
|
||||
{
|
||||
// Validazione e Cast in un colpo solo
|
||||
bool modeValido = Enum.IsDefined(typeof(QuestionModes), iMode);
|
||||
bool subModeValido = Enum.IsDefined(typeof(QuestionOrderSubModes), iSubMode);
|
||||
|
||||
if (modeValido && subModeValido)
|
||||
{
|
||||
var cMode = (QuestionModes)iMode;
|
||||
var cSubMode = (QuestionOrderSubModes)iSubMode;
|
||||
rKey = $"{cMode}:{cSubMode}:{UID}";
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!string.IsNullOrEmpty(rKey))
|
||||
{
|
||||
// cerco nella hash table RUN e nel caso sposto a DONE...
|
||||
string cQueue = qKey(QueueType.running);
|
||||
var hTask = await _db.HashGetAsync(cQueue, rKey);
|
||||
if(hTask.HasValue )
|
||||
{
|
||||
await SetDone($"{hTask}", QueueType.running);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Private Methods
|
||||
}
|
||||
}
|
||||
@@ -186,90 +186,6 @@ namespace Lux.API.Controllers
|
||||
}
|
||||
return Ok(rawData);
|
||||
}
|
||||
#if false
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Ritorno calcolo
|
||||
/// </summary>
|
||||
/// <param name="id">Chiave auth valida (da implementare)</param>
|
||||
/// <param name="retData"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("jobreturn/{id}")]
|
||||
public async Task<bool> JobReturn(string id, [FromBody] AnswerDTO retData)
|
||||
{
|
||||
bool saved = false;
|
||||
// se risposta valida --> processo!
|
||||
if (retData != null)
|
||||
{
|
||||
saved = await EMProc.ProcessAnswer(retData);
|
||||
#if DEBUG
|
||||
// se in debug forzo save dell'ultima chiamata comunque...
|
||||
saved = false;
|
||||
#endif
|
||||
// se non avessi salvato e non sapessi cosa fosse --> salvo comunque x debug...
|
||||
if (!saved && retData != null)
|
||||
{
|
||||
string UID = retData.Args["UID"];
|
||||
var envir = retData.ExecEnvironment;
|
||||
// salvo in area generica...
|
||||
string rawData = JsonConvert.SerializeObject(retData);
|
||||
await EMProc.SaveRawData(UID, retData.ExecEnvironment, rawData);
|
||||
}
|
||||
}
|
||||
// ritorno
|
||||
return saved;
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("jobcreate/{id}")]
|
||||
public async Task<bool> JobCreate(string id, [FromBody] AnswerDTO retData)
|
||||
{
|
||||
bool saved = false;
|
||||
// se risposta valida --> processo!
|
||||
if (retData != null)
|
||||
{
|
||||
saved = await EMProc.ProcessAnswer(retData);
|
||||
// se in debug forzo save dell'ultima chiamata comunque...
|
||||
saved = false;
|
||||
// se non avessi salvato e non sapessi cosa fosse --> salvo comunque x debug...
|
||||
if (!saved && retData != null)
|
||||
{
|
||||
string UID = retData.Args["UID"];
|
||||
var envir = retData.ExecEnvironment;
|
||||
// salvo in area generica...
|
||||
string rawData = JsonConvert.SerializeObject(retData);
|
||||
await EMProc.SaveRawData(UID, retData.ExecEnvironment, rawData);
|
||||
}
|
||||
}
|
||||
// ritorno
|
||||
return saved;
|
||||
}
|
||||
|
||||
[HttpPost("jobestimate/{id}")]
|
||||
public async Task<bool> JobEstimate(string id, [FromBody] AnswerDTO retData)
|
||||
{
|
||||
bool saved = false;
|
||||
// se risposta valida --> processo!
|
||||
if (retData != null)
|
||||
{
|
||||
saved = await EMProc.ProcessAnswer(retData);
|
||||
// se in debug forzo save dell'ultima chiamata comunque...
|
||||
saved = false;
|
||||
// se non avessi salvato e non sapessi cosa fosse --> salvo comunque x debug...
|
||||
if (!saved && retData != null)
|
||||
{
|
||||
string UID = retData.Args["UID"];
|
||||
var envir = retData.ExecEnvironment;
|
||||
// salvo in area generica...
|
||||
string rawData = JsonConvert.SerializeObject(retData);
|
||||
await EMProc.SaveRawData(UID, retData.ExecEnvironment, rawData);
|
||||
}
|
||||
}
|
||||
// ritorno
|
||||
return saved;
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Chiamata GET: num richieste in coda (se non specificato è waiting, altrimenti )
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Version>0.9.2601.1910</Version>
|
||||
<Version>0.9.2601.1915</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -18,14 +18,17 @@ namespace Lux.API.Services
|
||||
/// </summary>
|
||||
/// <param name="imgService"></param>
|
||||
/// <param name="dlService"></param>
|
||||
public ExternalMessageProcessor(ImageCacheService imgService, DataLayerServices dlService, CalcRuidService crService)
|
||||
public ExternalMessageProcessor(ImageCacheService imgService, DataLayerServices dlService, CalcRuidService crService, ProdService prodService)
|
||||
{
|
||||
cacheService = imgService;
|
||||
dbService = dlService;
|
||||
_calcRuidService = crService;
|
||||
_prodService = prodService;
|
||||
}
|
||||
|
||||
private readonly CalcRuidService _calcRuidService;
|
||||
private readonly ProdService _prodService;
|
||||
|
||||
#endregion Public Constructors
|
||||
|
||||
#region Public Methods
|
||||
@@ -106,7 +109,10 @@ namespace Lux.API.Services
|
||||
{
|
||||
RUID = retData.Args["RUID"];
|
||||
// salvo SUBITO chiusura statistiche...
|
||||
await _calcRuidService.CompleteRequestAsync(RUID);
|
||||
string hKey = await _calcRuidService.CompleteRequestAsync(RUID);
|
||||
|
||||
// verifico eventuali spostamenti code calcolo
|
||||
await _prodService.UpdateQueue(hKey);
|
||||
}
|
||||
|
||||
// gestione ritorno preview SVG
|
||||
@@ -172,7 +178,7 @@ namespace Lux.API.Services
|
||||
Log.Error($"Eccezione in gestione Estimate{Environment.NewLine}{exc}");
|
||||
}
|
||||
// reinvio in redis (cache + channel)
|
||||
await cacheService.SaveProdEstimationAsync(UID, retData.ExecEnvironment, rawAnsw);
|
||||
await cacheService.SaveProdEstimateAsync(UID, retData.ExecEnvironment, rawAnsw);
|
||||
saved = true;
|
||||
}
|
||||
|
||||
@@ -250,6 +256,7 @@ namespace Lux.API.Services
|
||||
return saved;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Salvataggio della risposta in formato RawData x Debug
|
||||
/// </summary>
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
<div class="row">
|
||||
<div class="col-12 my-1">
|
||||
<ol class="list-group">
|
||||
<li class="list-group-item align-items-lg-start bg-success text-light d-flex justify-content-between">
|
||||
<li class="list-group-item align-items-lg-start @TitleCss text-light d-flex justify-content-between">
|
||||
<div class="px-0">Coda Job @QueueType</div>
|
||||
@if (QueueAll.Count > 0 && EnableReset)
|
||||
@if (QueueAll.Count > 0)
|
||||
{
|
||||
<div class="px-0">
|
||||
<button class="btn btn-sm btn-warning" @onclick="() => ResetQueue()">Reset All <i class="fa-solid fa-arrow-turn-up"></i></button>
|
||||
@if (EnableReset)
|
||||
{
|
||||
<button class="btn btn-sm btn-warning" @onclick="() => ResetQueue()">Reset All <i class="fa-solid fa-arrow-turn-up"></i></button>
|
||||
}
|
||||
else if (EnableClear)
|
||||
{
|
||||
<button class="btn btn-sm btn-danger" @onclick="() => ResetQueue()">Clear Queue <i class="fa-solid fa-xmark"></i></button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</li>
|
||||
@if (QueuePaged == null || QueuePaged?.Count() == 0)
|
||||
{
|
||||
<li class="list-group-item align-items-lg-start">Nessun Job Eseguito!</li>
|
||||
<li class="list-group-item align-items-lg-start">Nessun Job trovato!</li>
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -24,7 +31,10 @@
|
||||
@item
|
||||
</div>
|
||||
<div class="px-0">
|
||||
<button class="btn btn-sm btn-primary" @onclick="() => ReRunJob(item)"><i class="fa-solid fa-share-from-square" title="Riesecuzione Estimate"></i> Re-Run</button>
|
||||
@if (EnableReRun)
|
||||
{
|
||||
<button class="btn btn-sm btn-primary" @onclick="() => ReRunJob(item)"><i class="fa-solid fa-share-from-square" title="Riesecuzione Estimate"></i> Re-Run</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@@ -56,9 +66,18 @@
|
||||
[Parameter]
|
||||
public string QueueType { get; set; } = "Run";
|
||||
|
||||
[Parameter]
|
||||
public string TitleCss { get; set; } = "bg-success";
|
||||
|
||||
[Parameter]
|
||||
public bool EnableClear { get; set; } = false;
|
||||
|
||||
[Parameter]
|
||||
public bool EnableReset { get; set; } = false;
|
||||
|
||||
[Parameter]
|
||||
public bool EnableReRun { get; set; } = false;
|
||||
|
||||
#endregion Public Properties
|
||||
|
||||
#region Protected Methods
|
||||
@@ -103,7 +122,7 @@
|
||||
/// <summary>
|
||||
/// elementi per pagina
|
||||
/// </summary>
|
||||
private int numRecord = 10;
|
||||
private int numRecord = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Numero tot records
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
@* <div class="col-12">
|
||||
<ol class="list-group">
|
||||
<li class="list-group-item align-items-lg-start text-bg-warning d-flex justify-content-between">
|
||||
<div class="px-0">Job in Attesa</div>
|
||||
@@ -25,20 +25,20 @@
|
||||
<ol class="list-group">
|
||||
<li class="list-group-item align-items-lg-start bg-success text-light d-flex justify-content-between">
|
||||
<div class="px-0">Job Eseguiti</div>
|
||||
@if (QueueRun.Count > 0)
|
||||
@if (QueueDone.Count > 0)
|
||||
{
|
||||
<div class="px-0">
|
||||
<button class="btn btn-sm btn-warning" @onclick="() => ResetRunQueue()">Reset All <i class="fa-solid fa-arrow-turn-up"></i></button>
|
||||
</div>
|
||||
}
|
||||
</li>
|
||||
@if (QueueRunPaged == null || QueueRunPaged?.Count() == 0)
|
||||
@if (QueueDonePaged == null || QueueDonePaged?.Count() == 0)
|
||||
{
|
||||
<li class="list-group-item align-items-lg-start">Nessun Job Eseguito!</li>
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var item in QueueRunPaged)
|
||||
foreach (var item in QueueDonePaged)
|
||||
{
|
||||
<li class="list-group-item align-items-lg-start">
|
||||
<div class="d-flex justify-content-between">
|
||||
@@ -59,29 +59,14 @@
|
||||
<EgwCoreLib.Razor.DataPager currPage="@currPage" PageSize="@numRecord" totalCount="@totalCount" numPageChanged="SavePage" numRecordChanged="SaveNumRec"></EgwCoreLib.Razor.DataPager>
|
||||
</nav>
|
||||
}
|
||||
@* <nav>
|
||||
<ul class="pagination mt-1">
|
||||
|
||||
<li class="page-item @(CurrentPage == 1 ? "disabled" : "")">
|
||||
<button class="page-link" @onclick="() => GoToPage(CurrentPage - 1)">«</button>
|
||||
</li>
|
||||
|
||||
@for (int i = 1; i <= TotalPages; i++)
|
||||
{
|
||||
<li class="page-item @(CurrentPage == i ? "active" : "")">
|
||||
<button class="page-link" @onclick="() => GoToPage(i)">@i</button>
|
||||
</li>
|
||||
}
|
||||
|
||||
<li class="page-item @(CurrentPage == TotalPages ? "disabled" : "")">
|
||||
<button class="page-link" @onclick="() => GoToPage(CurrentPage + 1)">»</button>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</nav> *@
|
||||
|
||||
</div> *@
|
||||
<div class="col-12 mt-2 mb-0">
|
||||
<JobQueue QueueType="Waiting" QueueAll="QueueWait" TitleCss="bg-warning" EnableClear="true" EC_ResetQueue="ResetQueue"></JobQueue>
|
||||
</div>
|
||||
<div class="col-12 mt-2 mb-0">
|
||||
<JobQueue QueueType="Done" QueueAll="QueueDone"></JobQueue>
|
||||
<JobQueue QueueType="Running" QueueAll="QueueRun" TitleCss="bg-success" EnableReset="true" EC_ResetQueue="ResetQueue"></JobQueue>
|
||||
</div>
|
||||
<div class="col-12 mt-2 mb-0">
|
||||
<JobQueue QueueType="Done" QueueAll="QueueDone" TitleCss="bg-secondary" EnableReRun="true" EC_ReRunJob="ReRunJob"></JobQueue>
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,7 +82,6 @@ namespace Lux.UI.Components.Compo.WorkLoad
|
||||
|
||||
#region Private Properties
|
||||
|
||||
private IEnumerable<string>? QueueRunPaged { get; set; } = null;
|
||||
private IEnumerable<string>? QueueDonePaged { get; set; } = null;
|
||||
|
||||
#endregion Private Properties
|
||||
@@ -95,12 +94,33 @@ namespace Lux.UI.Components.Compo.WorkLoad
|
||||
private void UpdateTable()
|
||||
{
|
||||
// fix paginazione
|
||||
QueueRunPaged = QueueRun
|
||||
QueueDonePaged = QueueDone
|
||||
.Skip(numRecord * (currPage - 1))
|
||||
.Take(numRecord);
|
||||
totalCount = QueueRun.Count();
|
||||
totalCount = QueueDone.Count();
|
||||
}
|
||||
|
||||
#endregion Private Methods
|
||||
|
||||
/// <summary>
|
||||
/// Esegue reset secondo tipo richiesta e coda
|
||||
/// </summary>
|
||||
/// <param name="qType"></param>
|
||||
/// <returns></returns>
|
||||
private async Task ResetQueue(string qType)
|
||||
{
|
||||
// reset secondo richiesta...
|
||||
switch (qType)
|
||||
{
|
||||
case "Waiting":
|
||||
await ResetWaitQueue();
|
||||
break;
|
||||
case "Running":
|
||||
await ResetRunQueue();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>aspnet-Lux.UI-a758c101-a2f4-4e38-977d-1c4887dbbd50</UserSecretsId>
|
||||
<Version>0.9.2601.1910</Version>
|
||||
<Version>0.9.2601.1915</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<body>
|
||||
<i>LUX - Web Windows MES</i>
|
||||
<h4>Versione: 0.9.2601.1910</h4>
|
||||
<h4>Versione: 0.9.2601.1915</h4>
|
||||
<br /> Note di rilascio:
|
||||
<ul>
|
||||
<li>
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.9.2601.1910
|
||||
0.9.2601.1915
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<item>
|
||||
<version>0.9.2601.1910</version>
|
||||
<version>0.9.2601.1915</version>
|
||||
<url>http://nexus.steamware.net/repository/SWS/GPW/stable/GPW.UI.zip</url>
|
||||
<changelog>http://nexus.steamware.net/repository/SWS/GPW/stable/ChangeLog.html</changelog>
|
||||
<mandatory>false</mandatory>
|
||||
|
||||
Reference in New Issue
Block a user